KVO实现MVVM

文章出处

为什么要用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只是提供多了一个不错的选择,要不要使用它,还是要看具体的项目而定。但是用上了,就停不下来了。

参考文章

Model-View-ViewModel for iOS

MVVM 介绍

被误解的MVC和被神化的MVVM

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容