[干货分享]一篇可能会让你爱上MVVM与ReactiveCocoa的文章

概要

在此工程中,本文将讨论将MVC改造为MVVM须要的一些基本方法,同时会适当穿插部分关于MVVM概念性的讨论!本文最大的意义在于,提供了一种读者能够复现的方式,逐步引出从MVC向MVVM尽量平滑过渡的一种方案;此外,也是为数很少的ReactiveCocoa实例文章之一.本文是MVVM系列文文章的第二篇,在阅读以前,您可能须要先阅读下第一篇文章: 写给iOS小白的MVVM教程(一): 从MVC到MVVM之一个典型的MVC应用场景php

Apple自己的UIKit框架是为MVC模式设计的,因此你在无形之中写就的代码其实就是MVC,并且你甚至会以为代码就应该这么写,不这么写还能怎么写?!MVVM因为缺少框架级别的支持,因此在iOS的开发中一直彷佛是很鸡肋式的存在.直到出现了ReactiveCocoa!它从框架界别支持MVVM模式,它让你真切地感受到本身之前的代码真的太乱了,它也让你真正有兴趣去尝试下一些比较流行的编程模式,好比响应式,函数式,MVVM等.出于本身的实际项目须要,必须最低支持 iOS 7版本,因此在进行本文以前,先对 RAC(ReactiveCocoa的简称,后文同)做了一番研究.虽然官方文档指明 3.0版本的RAC,最低支持的 是iOS 8.0,可是咱们依然能够经过 CocoaPods 安装 2.5版本的ReactiveCocoa来在本身的项目中使用,具体细节参见: ReactiveCocoa,最受欢迎的iOS函数响应式编程库(2.5版),没有之一!html

基本概念篇: MVC VS MVVM

MVC

提到MVC,你如今能够先本身回想一下本身写过的程序,而后再往下看.react

  • M 指的是Model,数据模型,它能够是一个系统自身的类型,好比字符串,数组等,也能够是一个自定义的类型. 以上篇文章为例,你能够认为 YFMVCPostListViewController 的 categoryName 属性是一个Model,也能够认为 articles 属性是Model.Model 就是那个用来存储数据的东西.
  • V,指的是View,通俗点说,全部UIView及其子类都属于V部分.
  • C,指的就是UIViewController 及其子类.

因此说, UIKit自身就是为MVC模式设计的,而你就算不清除什么是MVC,但你的代码其实就是MVC模式.当你阅读本身之前的代码或者别人的代码时,常常感受这个代码写的好乱(shi)啊,其实这真的不是本身或别人的锅,这是MVC自己难以免甚至必然会出现的一个坑!因此,后来有人借鉴其余语言,提出了MVVM模式,并躬身实践!ios

MVVM

首先,MVVM,从概念说上来讲,真的很好,很吸引人,即便你可能看不太懂,也感受很高大上的样子!可是,当你真的去百度相关概念时,每每会很纳闷,彷佛比我如今还麻烦,甚至开始怀疑,MVVM应该还只停留在理论阶段吧!--NO,只是由于你没有找到合适的文章,没有找到合适的工具--ReactiveCocoa!仍是先说一下 MVVM的基础概念吧,否则无法往下说了:git

  • 第一个M,和MVC中的M基本同样.可是要求更轻量级.MVC中的M,你能够会放一些和原始数据不相关的推断出来的属性或者工具方法,如Person类,你可能给他写一个方法来根据原始数据年龄来判断是否有资格作某事,好比结婚;可是MVVM中的M,根据个人理解,你直接用它来存放元数据(这里,可能仍是有争议的,仅是我的的理解与实践).
  • 第一个V,比MVC中的V要更普遍些,它包括 UIView 与 UIViewController及其子类,View用来显示和交互,UIViewController担当一种相似于桥梁的角色,来使 View 和 ViewModel部分更好通讯.
  • 余下的"VM",实际上是一个总体,指的是ViewModel,视图数据模型.若是你之前的许多代码都放在Model中,好比没有数据自动联网请求相关的数据什么的话,那你的那个Model其实和这个ViewModel有些像.MVVM中,要求Model更薄,最好只存储原始数据信息;而对于其余的设计到逻辑的代码,建议都放到ViewModel中.你可能会说,这样ViewModel 会不会很乱呢?未必!ViewModel中的代码会不少,可是ViewModel的可复用性和灵活性要远远大于ViewController.更具体点说,之前的一个控制器里面的代码,如今可能会被拆分到1个甚至多个ViewModel中,并且你的ViewModel不只这个控制器能够用,其余的控制器也能够用.虽然从单个控制器的逻辑代码量来看,优化不是很显著,可是ViewModel的模块化特性,将在涉及到页面复用以及后期维护时,让人感受心旷神怡!

关于MVVM,网上还有一种观点是,其实能够不要Model层,直接使用ViewModel层来存储数据.我的感受,若是考虑到单元测试,此时若是有单独的Model部分,能够根据一个Model,直接测试ViewModel的逻辑,是极好的,因此目前仍是继续保留Model部分.另外,也是考虑到后期可能会设计到Model自己的变动,好比将Model由一个普通的NSObjet变为CoreData的一个实体,能够很容易地让代码支持本地化.github

此时,我还在考虑的一点是,公司代码其实Model部分不是由我负责的,若是想继续引入MVVM改造项目,保留一个ViewModel层,也可使个人代码对其余项目成员的影响降到最低.想来也是极好的!web

变革: 从MVC到MVVM

接下来,会以第一篇文章的示例为基础,将逐步改造为MVVM模式.编程

为View写的数据模型: Model --> Model + ViewModel

个人观点是,尽可能不要使用系统自带的数据类型,好比数组,字典等做为Model,要尽量地使用自定义地类.使用自定义的类,方便后期维护,也能够避免一些基础错误,如:自定义的类,若是属性不匹配会编译失败,可是若是使用字典类型,key不匹配时,是不会有任何提示的(用过字典的童鞋,都懂我意思的吧).因此咱们此处要:数组

  • 新增Model: YFCategoryArticleListModel,表示按分类分组的文章列表,其中有两个字段:category,分类;articles,此分类下的文章列表.
  • 新增ViewModel: YFBlogDetailViewModel 表示文章的视图模型;YFBlogListViewModel 表示 分类文章列表的视图模型; YFBlogListItemViewModel 表示文章列表单个单元格的视图模型;

Model仅用于存储数据,ViewModel的具体逻辑下面须要时,会具体分析.另外,必须提到一点的是 @青玉伏案,给我推荐了一个RAC的VM框架ReactiveViewModel,有兴趣的能够研究下.可是我不是很能理解这么作的必要性,因此暂时我仍是按照我本身的理解,用最常规的方式来写ViewModel部分.网络

使用ViewModel做为模块入口: M + C --> VM + C

就像我开篇序言中提到的那样,MVVM系列的文章,不仅仅是关于MVVM的讨论,更是关于如何将已有MVC项目逐步过渡为MVVM架构的可行性以及方法步骤的探究.这里我采用的是一种折中的更具可行性的方案: 我对外暴露的接口是ViewModel,可是对应的会给这个ViewModel提供一个使用Model做为参数的便利初始化方法;控制器或模块内部,就直接使用传入的ViewModel.这样,我以为才是极好的,一方面本身能够践行MVVM,提早踩踩坑,另外一方面也基本不会对其余小伙伴的开发工做形成太多的困扰!具体到本文示例,具体指:

  • 文章列表控制器: 为了与MVC模式区分,新建控制器YFMVVMPostListViewController,并添加够公有属性viewModel,它是YFCategoryArticleListViewModel 类型.
  • 文章详情控制器: 为了与MVC模式区分,新建控制器YF
  • MVVMPostListViewController,仅添加只读属性viewModel,它是YFArticleViewModel类型.

关于ViewModel的自定义下面会具体谈到.

实现ViewModel.

必须指出的一点是: ViewModel是为View服务的,它的命名和字段定义应该根据View的须要来进行.本例是一个很是简单的场景.在复杂的场景中,一个model可能对应多个viewModel,此时多个视图可能都是同一种数据的不一样展现方式;一个viewModel可能对应多个model,此时页面比较复杂,设计到多种数据的展现.简言之,应该是一个View对应一个ViewModel(这一点,可能也有待商榷,但暂时我会采起此种方式).因此,你的ViewModel中的属性没必要和某个Model有真正意义上的对应关系,而是应该根据它服务的View来写和命名.

YFBlogListItemViewModel 博客列表单个单元格的视图模型

  • 添加属性intro: 这个viewModel 供展现博客列表中的单个单元格使用,但根据目前的UI显示,只须要一个字段便可,咱们给它命名为 intro,字符串属性.这个后期能够根据UI变化动态更改.就像上面提到的,ViewModel是为Model服务的.
  • 添加属性 blogId.
  • 添加初始化方法 -initWithArticleModel: 以便于从一个YFArticleModel对象构建视图模型.
  • 注意须要在初始化时设置 introl和model的title,desc属性的级联关系(我喜欢这么称呼,意会,有点重写getter方法的感受).这一步原本是在Controller中完成的,如今挪到了 ViewModel中,Controller 不就瘦了一点了吗,并且把这个逻辑写到这里还更方便代码复用.
- (instancetype)initWithArticleModel:(YFArticleModel *)model
{
    self = [super init];
    
    if (nil != self) {
        // 设置intro属性和model的属性的级联关系.
        RAC(self, intro) = [RACSignal combineLatest:@[RACObserve(model, title), RACObserve(model, desc)] reduce:^id(NSString * title, NSString * desc){
            NSString * intro = [NSString stringWithFormat: @"标题:%@ 内容:%@", model.title, model.desc];
            
            return intro;
        }];
        
        // 设置self.blogId与model.id的相互关系.
        [RACObserve(model, id) subscribeNext:^(id x) {
            self.blogId = x;
        }];
    }
    
    return self;
}

YFBlogListViewModel 博文列表的视图模型.

  • 添加属性blogListItemViewModels,NSArray 类型,用于存储文章列表单元格的视图模型.视图部分检测它的变化,而后动态刷新视图便可.
  • 添加工具方法: -first 与 -next,用于支持常见的数据分页操做,配合blogListItemViewModels,能够实现常见的上拉刷新与加载加载的操做.
  • 添加初始化方法 -initWithCategoryArtilceListModel, 用于快速使用一个分类文章列表数据模型来快速初始化.再次强调一次: model 和 viewModel 并非一一对应的关系,这里只是为了简化从一种Model生成此种ViewModel的操做;即,之后若是有其余种类的可使用此种ViewModel的话,咱们再为其添加一个重新Model初始化的方法便可.
  • 初始化时,涉及到网络请求,在此处咱们额外引入了一个AFN扩展 AFNetworking-RACExtensions,用于使用RAC的语法格式使用AFN.
// 接口完整地址,确定是受分类和页面的影响的.可是由于分类的变化最终会经过分页的变化来体现,因此此处仅需监测分页的变化状况便可.
[RACObserve(self, nextPageNumber) subscribeNext:^(NSNumber * nextPageNumber) {
    NSString * path = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostListViewController&model[category]=%@&model[page]=%@", self.category, nextPageNumber];
    
    self.requestPath = path;
}];
    
// 每次数据完整接口变化时,必然要同步更新 blogListItemViewModels 的值.
[RACObserve(self, requestPath) subscribeNext:^(NSString * path) {
    /**
     *  分两种状况: 若是是变为0,说明是重置数据;若是是大于0,说明是要加载更多数据;不处理向上翻页的状况.
     */
    
    NSMutableArray * articls = [NSMutableArray arrayWithCapacity: 42];
    
    if (YES != [self.nextPageNumber isEqualToNumber: @0]) {
        [articls addObjectsFromArray: self.blogListItemViewModels];
    }
    
    [[self.httpClient rac_GET:path parameters:nil] subscribeNext:^(RACTuple *JSONAndHeaders) {
        // 使用MJExtension将JSON转换为对应的数据模型.
        NSArray * newArticles = [YFArticleModel objectArrayWithKeyValuesArray: JSONAndHeaders.first];
        
        // RAC 风格的数组操做.
        RACSequence * newblogViewModels = [newArticles.rac_sequence
                                map:^(YFArticleModel * model) {
                                    YFBlogListItemViewModel * vm = [[YFBlogListItemViewModel alloc] initWithArticleModel: model];
                                    
                                    return vm;
                                }];
        
        
        [articls addObjectsFromArray: newblogViewModels.array];
        
        self.blogListItemViewModels = articls;
    }];
}];

关于MVVM的优点,此处已可见一斑!咱们成功的从控制器中剥离了网络请求以及数据分页的相关代码.从总体代码量的角度,咱们可能没少写几行代码;可是从代码复用性的角度考虑,咱们的代码更具备可复用性,由于未来可能其余地方也会用到这个页面;与此同时,代码之间的耦合性也下降了不少;可扩展性大大提升[PS: 关于代码耦合性,可复用性什么的,真的很大程度上是由模式自己决定的!]

YFBlogDetailViewModel 文章详情页的视图模型.

  • 添加属性content,用于直接在网页视图上显示,View内检测这个属性值,动态刷新视图便可.
  • 添加初始化方法 -initWithModel: 用于方便从一个 YFArticleModel 数据模型新建相应的视图模型.
  • 设计到网络请求部分的核心代码以下:
/**
 *  公共的与Model无关的初始化.
 */
- (void)setup
{
    // 初始化网络请求相关的信息.
    self.httpClient = [AFHTTPRequestOperationManager manager];
    self.httpClient.requestSerializer = [AFJSONRequestSerializer serializer];
    self.httpClient.responseSerializer = [AFJSONResponseSerializer serializer];
    
    // 接口完整地址,确定是受id影响.
    [RACObserve(self, blogId) subscribeNext:^(NSString * blogId) {
        NSString * path = [NSString stringWithFormat: @"http://www.ios122.com/find_php/index.php?viewController=YFPostViewController&model[id]=%@", blogId];
        
        self.requestPath = path;
    }];
    
    // 每次完整的数据接口变化时,必然要同步更新 self.content 的值.
    [RACObserve(self, requestPath) subscribeNext:^(NSString * path) {
        [[self.httpClient rac_GET:path parameters:nil] subscribeNext:^(RACTuple *JSONAndHeaders) {
            // 使用MJExtension将JSON转换为对应的数据模型.
            YFArticleModel * model = [YFArticleModel objectWithKeyValues:JSONAndHeaders.first];
           
            self.content = model.body;
        }];
    }];
}

若是耐心比较下 -setup 方法中的代码,会发现与上个VM的-setup有许多共同之处,这就启发咱们,或许应该将网络请求类从VM中进一步剥离出来,制做一个通用的网络请求类.通用网络请求类与单元测试的相关话题,会在下篇MVVM系列文章中专门讲述,在此再也不继续讨论.

不要为了RAC而RAC: 其实你可使用你熟悉的方式写View的.

坦白说,RAC真的让人很喜欢;可是,我在这里想说的是, RAC 只是简化编码的工具而已--所谓工具,就是那种你掌握了能够走的更快,不会也无伤大雅的东西!国内,部分文章过度渲染 RAC 与UIKit 的差别,甚至有人宣称是另外一条彻底不一样的学习曲线--真的很扯,逻辑上无异于就像宣称没有MFC,全部人都会饿死同样!在此,就不过多吐槽了,反正我是很早就看过国内某些博主的关于RAC的文章,被博主忽悠忽悠的不行,最终得出的结论是,太难了,暂时不学!若是,你恰好看到这篇文章,我想对你说的是: 耐下心,花一两天结合本身的工程和基础的RAC语法,尝试用RAC写写代码试试,真的很赞,并且是有足够的姿式彻底兼容之前的本身写法的!View部分,在此我就暂时不用RAC中的写法来替代block,代理等,尽量地在MVC的代码上,适当修正,以证实两者的某种程度上的协同做用.

控制器中的代码,真的被精简了很多,以博客列表控制器为例,几乎占据1/2控制器代码量的网络请求与数据分页的代码,被简化为一句话:

[RACObserve(self.viewModel, blogListItemViewModels) subscribeNext:^(id x) {
    [self updateView];
}];

一样的,博客详情也精简了很是多,忍不住想晒下完整代码:

//
//  YFMVVMPostViewController.m
//  iOS122
//
//  Created by 颜风 on 15/10/21.
//  Copyright (c) 2015年 iOS122. All rights reserved.
//

#import "YFMVVMPostViewController.h"
#import "YFBlogDetailViewModel.h"
#import <ReactiveCocoa.h>

@interface YFMVVMPostViewController ()
@property (strong, nonatomic) UIWebView * webView;
@end

@implementation YFMVVMPostViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [RACObserve(self.viewModel, content) subscribeNext:^(id x) {
        [self updateView];
    }];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (UIWebView *)webView
{
    if (nil == _webView) {
        _webView = [[UIWebView alloc] init];
        
        [self.view addSubview: _webView];
        
        [_webView makeConstraints:^(MASConstraintMaker *make) {
            make.edges.equalTo(UIEdgeInsetsMake(0, 0, 0, 0));
        }];
    }
    
    return _webView;
}

/**
 * 更新视图.
 */
- (void) updateView
{
    [self.webView loadHTMLString: self.viewModel.content baseURL:nil];
}

@end

相关文章
相关标签/搜索