之前一直有人问我:我知道MVC,我也知道MVVM。但是这两者在项目开发中怎么用?它们有什么区别?
好吧,今天就写一篇博客来解释这个问题,希望对这一类人有一定的帮助吧。在写之前的话我们也需要一些准备:MVC的理论知识、MVVM是如何演变过来的。同时,大家在字典转模型的过程中可以使用MJExtension。我是闲的蛋疼,使用runtime,自己写了一个字典转模型的基类,在之后的转化中,我就使用自己的方法了。
那么闲话少说,直接就切入正题。在做网络请求的过程中我们可以提前准备好网络请求的工厂类,你可以选择使用AFNetworking或ASI,我使用NSURLSession自己做了一个简单的请求封装。
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
- Model(模型)表示应用程序核心(比如数据库记录列表)。
- View(视图)显示数据(数据库记录)。
- Controller(控制器)处理输入(写入数据库记录)。
这里写链接内容
如果你看了这个还不明白MVC的基础知识的话:
倘若你还不知道什么事MVC的,请看斯坦福老爷子的视频教程:这里写链接内容
以上只是我们应用MVC的第一个步骤:理论。我们在项目中会怎么使用这样的一个模式呢?(以下是我以一个真实项目的登录作为一个例子来说明。当然一般而言,登录这些简单的网络请求是不会使用MVC的,所谓杀鸡焉用牛刀嘛,这里我们只是作为一个例子来使用。)首先,我们做一个简单的登录界面,两个TextField和一个Button:
当我们点击登录的时候进行网络请求。
完成登录按钮的事件关联。(这里我使用的是storyBoard做的页面,点击事件使用了RAC,所以你可能看不到很多的代码,我们重点是网络请求和数据解析。)
如果你做完了上述事情后,在viewDidLoad中的代码大致是这个样子的:
- (void)viewDidLoad {
[super viewDidLoad];
[[self.login rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
[HTTP postWithApi:@"http://xxxx.xxxx.com/terminal/login" withParameter:@{@"phoneNumber":self.userName.text,@"password":self.password.text} withSuccessBlock:^(id result) {
if ([result[@"status"] intValue] == 1) {
/**1**/
}
} withFailureBlock:^(NSError *error) {
}];
}];
}
这时候,我们打印出result的结果:
Printing description of result:
{
"access_token" = 9b1a3387185dcafd8a2575824c730fac;
message = "\U767b\U5f55\U6210\U529f";
result = {
gender = "\U7537";
id = 491;
password = 69fb7353edad81d92d6e60f246b7bea0;
phoneNumber = xxxxxxxxxxx;
};
status = 1;
}
我们可以看到它最外层是一个字典,然后result关键字所对应的值也是字典。如果我要取到gender或者result值中的其他量的话,我们可以在“1”处添加这样的代码
id gender = result[@"result"][@"gender"];
看起来还行,因为我们的数据结构简单。试想一下,如果我们在result中还嵌套了诸如数组、字典这类结构,然后再在这些结构里面各种嵌套。最后要取到最底层的某个值得话,代码是不是应该这样:
id obj = result[@"result"][@"..."]......[@"whatever"];
这中间有多少层,我就不清楚了,这取决于你于后台接口人员的协商了。是不是各种麻烦,再加上数组这些结构,over了,里面的东西就写得人蛋疼了。
好了,我们再来看看MVC如何实现呢?
我们新建一个LoginModel,由于我们现在要取到gender,还需要一个模型,我们暂时取名叫resultModel,我们来看看.h中如何写:
#import "YX_ObjectToModel.h"
@interface resultModel : YX_ObjectToModel
///
@property (copy, nonatomic) NSString * gender;
///密码
@property (copy, nonatomic) NSString * password;
///账户
@property (copy, nonatomic) NSString * phoneNumber;
@end
@interface LoginModel : YX_ObjectToModel
@property (copy, nonatomic) NSString * access_token;
@property (copy, nonatomic) NSNumber * status;
@property (strong, nonatomic) resultModel * resModel;
@end
这里注意,每一层的model中的属性名称,必须和请求结果中的key对应哦。我们用resModel来解析网络数据中的result值,但这里为什么属性名称又不叫result了呢?我们看看.m中如何实现的,兴许你就明白了:
#import "LoginModel.h"
@implementation resultModel
@end
@interface LoginModel ()
@property (strong, nonatomic) NSDictionary * result;
@end
@implementation LoginModel
- (void)setResult:(NSDictionary *)result {
if (result != _result) {
_result = result;
self.resModel = [resultModel modelWithDictionary:result];
}
}
@end
实际上我们还是有一个result的NSDictionary属性,只是没有对外公开而已,属于私有变量。通过他的setter方法,将resModel配置成功。哈哈,明白没有?没明白的话去学习Objective-C基础吧。
这时候我们在“1”处只需要这么写:
LoginModel * lo = [LoginModel modelWithDictionary:result];
我们要取到gender就很简单了:
id gender = lo.resModel.gender;
是不是很简单。这时候该知道MVC如何用,以及它的好处了吧。(这里提一个引申问题:MVC带来了一个缺点,请问是什么?)接下来,我们讲一下MVVM吧!
关于MVVM的知识讲解,来源以及实现又有比较详细的讲解。这里写链接内容
事实上,我们可以大致看出它就是在MVC的基础上在M和V中间插入了一个ViewModel用于做数据解析。
我们还是看看之前我们提出的引申问题吧:同样,如果网络数据中嵌套的层数很多的话,也就意味着我们需要建更多的模型来解析这个数据。如果套个四五层,哈哈,四五个模型也就出来了。光一个网络数据就搞了四五个模型,那么我们应对复杂的数据时工程类就太多了吧。
这时候我们来看看MVVM是怎么做的吧!
我们需要新建一个Model,用它来做网络请求,不再和以前的数据存储相同了。来看看.h如何设计吧:
#import <Foundation/Foundation.h>
@interface Login : NSObject
@property (strong, nonatomic, readonly) NSDictionary * dict;
- (void)getDataWithUserName:(NSString *)user withPassword:(NSString *)pass;
@end
是不是看起来很简单,就只有一个请求结果dict,和一个发送请求的方法。哦,为什么是readonly?
为了防止外部操作改变模型的结果,只允许model内部操作改变它,加个readonly更安全。
我们再来看看.m中如何实现:
#import "Login.h"
#import "HTTP.h"
@interface Login ()
// 这里需要改变为可读写状态,因为内部是可以让dict发生改变的。
@property (strong, nonatomic) NSDictionary * dict;
@end
@implementation Login
- (void)getDataWithUserName:(NSString *)user withPassword:(NSString *)pass {
[HTTP postWithApi:@"http://gk.rimiedu.com/terminal/login/patient" withParameter:@{@"phoneNumber":user,@"password":pass} withSuccessBlock:^(id result) {
if ([result[@"status"] intValue] == 1) {
self.dict = result;
}
} withFailureBlock:^(NSError *error) {
}];
}
@end
好了,模型要做的事情就over了。具体到数据解析类viewModel有该如何操作呢?我们先来看看.h如何设计的。
#import <Foundation/Foundation.h>
@interface LoginViewModel : NSObject
+ (NSString *)keyPath;
- (void)getDataWithUserName:(NSString *)user withPassword:(NSString *)pass;
- (id)gender;
@end
这里也需要一个方法来发送网络请求,实际上就是转一次收,让viewModel的model做一次网络请求,展示.m的时候你就会看到。由于网络请求是一个异步的过程,那么我们需要一个回调来告知controller,数据更改了,以便controller做出相应的操作。当然要到达该效果有很多种方法,block、通知、kvo都可以,这里我使用的是kvo,所以我写了一个工厂方法keyPath,以方便得到观察路径。最后还有一个获取指定变量的方法,该方法将解析过后的数据直接返回。
接下来我们来实现.m文件:
#import "LoginViewModel.h"
#import "Login.h"
@interface LoginViewModel ()
@property (strong, nonatomic) Login * model;
@end
@implementation LoginViewModel
- (Login *)model {
if (!_model) {
_model = [[Login alloc] init];
}
return _model;
}
+ (NSString *)keyPath {
return @"model.dict";
}
- (void)getDataWithUserName:(NSString *)user withPassword:(NSString *)pass {
[self.model getDataWithUserName:user withPassword:pass];
}
- (id)gender {
if ([self.model.dict[@"status"] intValue] == 1) {
return self.model.dict[@"result"][@"gender"];
}
return nil;
}
@end
这下是不是就一目了然了!!!
好了,我们做完了数据的model后viewModel后,回到controller中。这时候我们申明一个viewModel的实例对象,通过该对象发送网络请求,获取gender。
@property (strong, nonatomic) LoginViewModel * viewModel;
我们通过懒加载的方式加载处viewModel后就应该到相应的方法中完成请求了。在Button的点击事件中:
[[self.login rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(id x) {
[self.viewModel getDataWithUserName:self.userName.text withPassword:self.password.text];//进行网络请求
[self.viewModel addObserver:self forKeyPath:[LoginViewModel keyPath] options:NSKeyValueObservingOptionNew context:nil];//添加观察者,这里需要注意,添加了观察者之后要在dealloc中移除,并且要实现观察者的回调方法。
}];
实现观察者的回调方法:
//kvo回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
NSLog(@"%@",[self.viewModel gender]);
}
//移除观察者
- (void)dealloc {
[self.viewModel removeObserver:self forKeyPath:[LoginViewModel keyPath]];
}
程序跑起来之后,结果就出现了!!!
实际上在以前没有MVVM的时候有一种叫“胖model”的说法,就是将数据解析也放到model中,这样controller中的代码就比较少了。总观全局,我们可以看出MVC让我们的工程的每部分职责鲜明了,通过使用kvc的技术,让我们的数据解析工作变得十分的简单,减少了工程中大量的解析代码,但却增加了工程的类的数目。而MVVM不论怎么复杂的数据,都只需要两个文件做数据方面的事情model和viewModel。又减少了很大的工作量,但是如何让model和controller之间通讯又会有一定的问题,当然方法很多,但是对于新手朋友们而言就没有那么容易上手了。幸好RAC的出现让我们看到了胜利的曙光。