在此工程中,本文将讨论将MVC改造为MVVM需要的一些基本方法,同时会适当穿插部分关于MVVM概念性的讨论!本文最大的意义在于,提供了一种读者可以复现的方式,逐步引出从MVC向MVVM尽可能平滑过渡的一种方案;此外,也是为数不多的ReactiveCocoa实例文章之一.本文是MVVM系列文文章的第二篇,在阅读之前,您可能需要先阅读下第一篇文章: 写给iOS小白的MVVM教程(一): 从MVC到MVVM之一个典型的MVC应用场景
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版),没有之一!
提到MVC,你现在可以先自己回想一下自己写过的程序,然后再往下看.
所以说, UIKit自身就是为MVC模式设计的,而你就算不清除什么是MVC,但你的代码其实就是MVC模式.当你阅读自己以前的代码或者别人的代码时,经常感觉这个代码写的好乱(shi)啊,其实这真的不是自己或别人的锅,这是MVC本身难以避免甚至必然会出现的一个坑!所以,后来有人借鉴其他语言,提出了MVVM模式,并躬身实践!
首先,MVVM,从概念说上来说,真的很好,很吸引人,即使你可能看不太懂,也感觉很高大上的样子!但是,当你真的去百度相关概念时,往往会很纳闷,似乎比我现在还麻烦,甚至开始怀疑,MVVM应该还只停留在理论阶段吧!--NO,只是因为你没有找到合适的文章,没有找到合适的工具--ReactiveCocoa!还是先说一下 MVVM的基础概念吧,不然没法往下说了:
关于MVVM,网上还有一种观点是,其实可以不要Model层,直接使用ViewModel层来存储数据.个人感觉,如果考虑到单元测试,此时如果有单独的Model部分,可以根据一个Model,直接测试ViewModel的逻辑,是极好的,所以目前还是继续保留Model部分.另外,也是考虑到后期可能会设计到Model本身的变更,比如将Model由一个普通的NSObjet变为CoreData的一个实体,可以很容易地让代码支持本地化.
此时,我还在考虑的一点是,公司代码其实Model部分不是由我负责的,如果想继续引入MVVM改造项目,保留一个ViewModel层,也可以使我的代码对其他项目成员的影响降到最低.想来也是极好的!
接下来,会以第一篇文章的示例为基础,将逐步改造为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