在此工程中,本文将讨论将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,你如今能够先本身回想一下本身写过的程序,而后再往下看.react
因此说, UIKit自身就是为MVC模式设计的,而你就算不清除什么是MVC,但你的代码其实就是MVC模式.当你阅读本身之前的代码或者别人的代码时,常常感受这个代码写的好乱(shi)啊,其实这真的不是本身或别人的锅,这是MVC自己难以免甚至必然会出现的一个坑!因此,后来有人借鉴其余语言,提出了MVVM模式,并躬身实践!ios
首先,MVVM,从概念说上来讲,真的很好,很吸引人,即便你可能看不太懂,也感受很高大上的样子!可是,当你真的去百度相关概念时,每每会很纳闷,彷佛比我如今还麻烦,甚至开始怀疑,MVVM应该还只停留在理论阶段吧!--NO,只是由于你没有找到合适的文章,没有找到合适的工具--ReactiveCocoa!仍是先说一下 MVVM的基础概念吧,否则无法往下说了:git
关于MVVM,网上还有一种观点是,其实能够不要Model层,直接使用ViewModel层来存储数据.我的感受,若是考虑到单元测试,此时若是有单独的Model部分,能够根据一个Model,直接测试ViewModel的逻辑,是极好的,因此目前仍是继续保留Model部分.另外,也是考虑到后期可能会设计到Model自己的变动,好比将Model由一个普通的NSObjet变为CoreData的一个实体,能够很容易地让代码支持本地化.github
此时,我还在考虑的一点是,公司代码其实Model部分不是由我负责的,若是想继续引入MVVM改造项目,保留一个ViewModel层,也可使个人代码对其余项目成员的影响降到最低.想来也是极好的!web
接下来,会以第一篇文章的示例为基础,将逐步改造为MVVM模式.编程
个人观点是,尽可能不要使用系统自带的数据类型,好比数组,字典等做为Model,要尽量地使用自定义地类.使用自定义的类,方便后期维护,也能够避免一些基础错误,如:自定义的类,若是属性不匹配会编译失败,可是若是使用字典类型,key不匹配时,是不会有任何提示的(用过字典的童鞋,都懂我意思的吧).因此咱们此处要:数组
Model仅用于存储数据,ViewModel的具体逻辑下面须要时,会具体分析.另外,必须提到一点的是 @青玉伏案,给我推荐了一个RAC的VM框架ReactiveViewModel,有兴趣的能够研究下.可是我不是很能理解这么作的必要性,因此暂时我仍是按照我本身的理解,用最常规的方式来写ViewModel部分.网络
就像我开篇序言中提到的那样,MVVM系列的文章,不仅仅是关于MVVM的讨论,更是关于如何将已有MVC项目逐步过渡为MVVM架构的可行性以及方法步骤的探究.这里我采用的是一种折中的更具可行性的方案: 我对外暴露的接口是ViewModel,可是对应的会给这个ViewModel提供一个使用Model做为参数的便利初始化方法;控制器或模块内部,就直接使用传入的ViewModel.这样,我以为才是极好的,一方面本身能够践行MVVM,提早踩踩坑,另外一方面也基本不会对其余小伙伴的开发工做形成太多的困扰!具体到本文示例,具体指:
关于ViewModel的自定义下面会具体谈到.
必须指出的一点是: ViewModel是为View服务的,它的命名和字段定义应该根据View的须要来进行.本例是一个很是简单的场景.在复杂的场景中,一个model可能对应多个viewModel,此时多个视图可能都是同一种数据的不一样展现方式;一个viewModel可能对应多个model,此时页面比较复杂,设计到多种数据的展现.简言之,应该是一个View对应一个ViewModel(这一点,可能也有待商榷,但暂时我会采起此种方式).因此,你的ViewModel中的属性没必要和某个Model有真正意义上的对应关系,而是应该根据它服务的View来写和命名.
- (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; }
// 接口完整地址,确定是受分类和页面的影响的.可是由于分类的变化最终会经过分页的变化来体现,因此此处仅需监测分页的变化状况便可. [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: 关于代码耦合性,可复用性什么的,真的很大程度上是由模式自己决定的!]
/** * 公共的与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 只是简化编码的工具而已--所谓工具,就是那种你掌握了能够走的更快,不会也无伤大雅的东西!国内,部分文章过度渲染 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