为什么要用MVVM替代MVC
Apple倡导开发者们使用MVC模式开发App程序,但很多人都没有严格按照MVC的模式去开发,只是让程序的架构看上去像MVC,而实际上是MC或VC。
很多入门开发者都有一个通病,就是把所有的逻辑,界面生成都写进ViewController中,这样ViewController就变成了一个Massive View Controller(重量级视图控制器)。重量级视图控制器会让整个ViewController变得非常复杂且不可维护,让维护者崩溃却无从下手,只能忍痛默默的重写整个逻辑。
但是,有一种解决方案,可以解决Massive View Controller的问题,那就是MVVM。
这是传统MVC模式:
这是MVVM模式:
很多时候新手们会把数据转换逻辑,网络请求逻辑等都放到ViewController中,这样会不可避免的让ViewController变得臃肿,这是造成重量级视图控制器的重要原因。
除了上面提到的一个原因外,由于AFNetworking是iOS开发网络访问框架的事实标准,而AFNetworking使用的是block来实现网络回调,block会让block里面引用的变量的引用数+1,在某种网速非常缓慢的极端情况下,当用户打开ViewController的时候,网络请求已经发出,也就是说在block中的变量引用已经+1,如果此时用户退出这个ViewController,当这个block发生回调时,此时持有block里面变量的ViewController已经被回收,而block里面的变量由于block的原因没有被及时回收,这样会造成crash的。这是在使用block进行回调时很容易被忽略的情况。
对于业务而言,将所有业务不加区分都放进ViewController中,这显然是一种懒惰的表现。一个ViewController中包含大量业务的细节,将使这个ViewController在业务协调和调用中迷失,将让这个业务变得非常混乱,让业务逻辑变得无法维护。由于重量级ViewController的复杂性,其代码将难以复用。
以上原因是传统MVC难以解决的,为解决这些问题,要采用MVVM的开发模式。
MVVM不是什么新鲜事,简单说就是将部分逻辑从ViewController中拆分出来,并整合起来在ViewController和Model中间加多一个ViewModel,ViewModel不直接引用View,ViewController也不引用Model中的方法,所有网络回调数据处理等逻辑都放到ViewModel中,ViewController通过ViewModel来请求数据和更新数据。
如何实践MVVM
参考项目:https://github.com/britzlieg/MVVMDemo/tree/master
第一步:创建Model
AFNetworking请求方法放到Model中
@interface Model : NSObject
@property (nonatomic, copy) NSString *col;
@property (nonatomic, copy) NSString *sort;
@property (nonatomic, copy) NSString *tag3;
@property (nonatomic, assign) NSInteger startIndex;
@property (nonatomic, assign) NSInteger returnNumber;
@property (nonatomic, strong) NSArray *imgs;
@property (nonatomic, copy) NSString *tag;
@property (nonatomic, assign) NSInteger totalNum;
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail;
具体实现,不多说:
@implementation Model
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail {
NSString *urlString = [NSString stringWithFormat:@"%@%ld%@",
@"http://image.baidu.com/data/imgs?col=%e7%be%8e%e5%a5%b3&tag=%e5%b0%8f%e6%b8%85%e6%96%b0&sort=0&pn=1",
aPage,@"&rn=1&p=channel&from=1"];
AFHTTPRequestOperationManager *managere = [AFHTTPRequestOperationManager manager];
[managere GET:urlString parameters:nil
success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
success(responseObject,nil);
NSLog(@"success");
} failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) {
fail(nil,error);
NSLog(@"fail");
}];
}
@end
第二步:创建ViewModel
ViewModel的属性:
- data : 请求获取的数据
- racMsg : 请求成功和失败的信号量(主要用KVO对这个进行监视)
@interface ViewModel : NSObject
@property (strong,nonatomic) NSDictionary *data;
@property (strong,nonatomic) NSString *racMsg;
- (void)getImagesList;
- (void)getNextImagesList;
- (void)getPreImagesList;
@end
ViewController中主要监视ViewModel的racMsg来发现data更新。
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
@interface ViewModel()
@property (nonatomic) NSInteger currentPage;
@end
@implementation ViewModel
- (instancetype)init {
self = [super init];
self.currentPage = 0;
return self;
}
- (void)getImagesList {
WS(ws)
[Model getImagesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
- (void)getNextImagesList {
WS(ws)
self.currentPage++;
[Model getImagesListWithPage:self.currentPage
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
- (void)getPreImagesList {
WS(ws)
self.currentPage = self.currentPage == 0 ? 0 : self.currentPage-1;
[Model getImagesListWithPage:self.currentPage
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
@end
可能会有人觉得为什么不直接监视data,但我个人更倾向于采用一种类似于信号量的机制,监听特定的信号来更新数据。如果直接监视data,则data只有nil和非nil两种情况,要进一步区分请求的状态的话必须要对data进行解析,增加了转换成本,不如直接采用多一个属性变量进行判断和协调。
第三步:ViewController中KVO设置
ViewController直接持有viewModel
@interface ViewController ()
@property (strong,nonatomic) ViewModel *viewModel;
@property (strong,nonatomic) UITextView *showTextView;
@end
加载ViewController时初始化KVO和调用ViewModel方法getImagesList来请求数据
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// requestData
[self _initViews];
[self setupKVO];
[self.viewModel getImagesList];
}
ViewController销毁时去除KVO
- (void)dealloc {
[self removeKVO];
}
KVO相关的函数。observeValueForKeyPath只需对racMsg进行判断就可以知道data的值是否更新了,如果更新了就更新一下View。
#pragma mark - KVO
- (void)setupKVO {
[self.viewModel addObserver:self
forKeyPath:@"racMsg" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
}
- (void)removeKVO {
[self.viewModel removeObserver:self forKeyPath:@"racMsg"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
}
else {
_showTextView.text = @"error";
}
}
}
按钮点击事件和View的初始化
#pragma mark - Event Response
- (void)getPre {
[self.viewModel getPreImagesList];
}
- (void)getNext {
[self.viewModel getNextImagesList];
}
#pragma mark - Private
- (void)_initViews {
UIButton *preBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 50, 200, 40)];
[preBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[preBtn setTitle:@"Pre" forState:UIControlStateNormal];
[preBtn addTarget:self action:@selector(getPre) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:preBtn];
UIButton *nextBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 150, 200, 40)];
[nextBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[nextBtn setTitle:@"nextBtn" forState:UIControlStateNormal];
[nextBtn addTarget:self action:@selector(getNext) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:nextBtn];
_showTextView = [[UITextView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
_showTextView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:_showTextView];
}
实践MVVM的具体好处的例子
下面讨论一下用MVVM的具体好处的例子。
case 1:ViewController需要一个额外请求一个文章列表,这个文章列表的请求参数与当前请求图片列表的接口返回结果没有任何关联,可以并行请求。
在这种情况下,在ViewModel中加入方法getArticleList()和属性articleList以及articleMsg,然后在ViewController中需要调用该方法的位置调用该方法即可。KVO中的observeValueForKeyPath方法稍微修改一下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
}
可见并行业务功能上的扩展是非常简单的,整个Controller的总体逻辑几乎不用怎么变化,只需要改动局部细节即可。
case 2:同case 1,但是文章列表的请求参数需要通过图片列表接口返回的结果获取,请求是串联嵌套的(即先请求图片列表接口,请求完成后,根据返回结果再请求文章列表接口)。
对于嵌套的请求,如果采用传统MVC模式,就要在ViewController中加入两个block,一个嵌套另外一个,这样会让代码变得非常难看,而且会让子block依赖于父block,难以对其进行拆分。但如果采用MVVM,则会将所有的请求变化都置于KVO的监控之下,并作出统一的处理。
ViewModel中的与case 1一样,但ViewController中的处理稍微不同。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
// 请求文章
[_viewModel getArticleList];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
}
可以看出还是不需要改动大逻辑,即可对有依赖的业务进行扩展。
case 3: 同case 2,但是增加一个依赖于文章列表返回结果的评估列表接口(即图片->文章->评论)。
假设存在一种这样的情况,请求数据的顺序是:图片->文章->评论,这样的话如果用传统的MVC模式做的,就是三层block的嵌套,这对于一个ViewController来说是噩梦。三层嵌套,意味着无法复用,只能写死在这个ViewController之中。这时使用MVVM就显得非常必要了。
当出现三层以上的依赖时,其实可以考虑将所有依赖捆绑在一起,做成一个高内聚的模块,在MVVM中,由于请求数据的方法不是写在ViewController之中,所以将这三个模块进行内聚的工作是放到ViewModel之中。
ViewModel.m :
- (void)getCommentsList {
WS(ws)
[Model getImagesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
[Model getArticlesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.article = responseObjectDict;
ws.articleMsg = @"success";
[Model getCommentsListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.comments = responseObjectDict;
ws.commentsMsg = @"success"; // 成功
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.comments = nil;
ws.commentsMsg = @"fail"; // 失败
}];
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.article = nil;
ws.articleMsg = @"fail";
}];
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
ViewController.h中:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
// 请求文章
[_viewModel getArticleList];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"commentsMsg"]) {
if ([_viewModel.commentsMsg isEqualToString:@"success"]) {
_commentsTextView.text = _viewModel.commentsList
}
else {
_commentsTextView.text = @"error";
}
}
}
三层嵌套的block是无法避免的,但是MVVM可以将这个恶心的东西放到ViewModel中,而不是ViewController中,这样当ViewController要获取评论时,只需要调用viewModel的getCommentsList即可,不需要看到三层block请求的细节,这样可以很好的将逻辑与细节隔离。
可以看出使用MVVM还是能够很方便的扩展多层依赖的业务。
case 4: 另外一个ViewController需要调用图片接口获取数据
这种情况非常简单,直接在ViewController2中加入一个ViewModel的属性,其他按照ViewController中的调用方式调用即可。
由于将请求逻辑放到了ViewController,所以ViewModel对ViewController是没有依赖的,所以ViewController2能够很好的直接使用ViewModel,这是传统MVC很难做得到的。
case 5: 另外一个ViewController需要评论接口来获取数据(将三个接口请求过程内聚,便于其他复用)
这种情况也非常简单,与case 4是一样的,直接调用ViewModel中的getCommentsList(),而这个函数是已经在ViewModel中高内聚了,所以使用也非常方便,代码复用和请求逻辑复用都非常方便清晰!
case 6: ViewController中有很多数据转换逻辑,多个ViewController的数据转换逻辑都相同的情况。
ViewModel中不仅只包含请求逻辑,还可以包含数据转换的逻辑,还有一些不知要怎么归类的杂七杂八的逻辑。多个ViewController发生相同的数据转换的情况是经常会有的,如果把数据转换逻辑写到Controller之中,会让每一个Controller都持有一个转换逻辑,这对于数据转换逻辑的统一来说是非常糟糕的。如果把相同的数据转换逻辑都抽象封装到同一个ViewModel中,ViewController不直接持有数据转换逻辑,而是通过ViewModel来调用的话,每个ViewController只需要维护一个ViewModel实例即可,所有转换细节都可以在ViewModel中进行统一修改。Controller只关心数据和数据与View的交互,不应该关心数据之间的转换和数据怎样获取的,这应该是MVVM的一个原则。
总结
MVVM的核心在于绑定,本文采用的是KVO的绑定机制,能够很好与Objective-C和Cocoa结合起来,不需要借用第三方的类库进行数据绑定。
除了使用KVO,业界通常采用的是ReactiveCocoa。但是,ReactiveCocoa的学习成本过高,不适合轻量级的开发,而MVVM只是一种开发模式,并不是一种具体的框架,所以如果不是非常想深入使用MVVM的精髓的话,是没有必要去学习ReactiveCocoa的。网上还有一些讨论MVVM的博客提到ViewModel直接对View进行操作,其实这是一种很不严谨的做法,MVVM中的ViewModel不应该关心View的显示,只应该关心数据的获取和转换,View如何显示那是ViewController的职责。所以凡是在ViewModel中引用了UIKit的,个人认为都不是一种严格意义上的MVVM。
当然MVVM也有其自身的不足,比如引入ViewModel之后,文件数量增加了不少,总的代码量其实也会增加,这对于极简主义者来说并不是一种很好的模式。而且MVVM的开发思路与MVC是不同的,开发者要转换思路采用MVVM的开发方式其实还是有不少的学习成本,而且对于大部分简单业务来说,使用MVVM会增加业务的复杂度,显得臃肿和多余。本文中使用KVO的MVVM模式,从本质上来说其实是MVC的衍生,把C中的一部分拆分出来并隔离M和C,所以从模式上来说,这样是完全可以兼容传统的MVC开发模式的。因此,对于简单的业务,可以直接采用MVC的模式开发,不需要额外创建一个ViewModel。对于复杂业务,就采用KVO的MVVM模式,进行业务拆分和复用。这种折中的方法能够将MVC和MVVM的优点都利用起来,避免只使用一个造成开发效率上的降低。
MVVM不应该被误解和神化,使用MVVM只是提供多了一个不错的选择,要不要使用它,还是要看具体的项目而定。但是用上了,就停不下来了。