[干货分享]一篇可能会让你爱上MVVM与ReactiveCocoa的文章
概要
在此工程中,本文将讨论将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 VS MVVM
MVC
提到MVC,你现在可以先自己回想一下自己写过的程序,然后再往下看.
- M 指的是Model,数据模型,它可以是一个系统自身的类型,比如字符串,数组等,也可以是一个自定义的类型. 以上篇文章为例,你可以认为 YFMVCPostListViewController 的 categoryName 属性是一个Model,也可以认为 articles 属性是Model.Model 就是那个用来存储数据的东西.
- V,指的是View,通俗点说,所有UIView及其子类都属于V部分.
- C,指的就是UIViewController 及其子类.
所以说, UIKit自身就是为MVC模式设计的,而你就算不清除什么是MVC,但你的代码其实就是MVC模式.当你阅读自己以前的代码或者别人的代码时,经常感觉这个代码写的好乱(shi)啊,其实这真的不是自己或别人的锅,这是MVC本身难以避免甚至必然会出现的一个坑!所以,后来有人借鉴其他语言,提出了MVVM模式,并躬身实践!
MVVM
首先,MVVM,从概念说上来说,真的很好,很吸引人,即使你可能看不太懂,也感觉很高大上的样子!但是,当你真的去百度相关概念时,往往会很纳闷,似乎比我现在还麻烦,甚至开始怀疑,MVVM应该还只停留在理论阶段吧!--NO,只是因为你没有找到合适的文章,没有找到合适的工具--ReactiveCocoa!还是先说一下 MVVM的基础概念吧,不然没法往下说了:
- 第一个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的一个实体,可以很容易地让代码支持本地化.
此时,我还在考虑的一点是,公司代码其实Model部分不是由我负责的,如果想继续引入MVVM改造项目,保留一个ViewModel层,也可以使我的代码对其他项目成员的影响降到最低.想来也是极好的!
变革: 从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
附
- 示例完整代码: iOS122
- 推荐阅读: 基于AFNetworking2.0和ReactiveCocoa2.1的iOS REST Client
- 下篇主题: MVVM系列的下一个主题,会对MVVM中网络请求的逻辑进一步从VM剥离以及单元测试的一些技术性讨论,欢迎回帖交流!
[干货分享]一篇可能会让你爱上MVVM与ReactiveCocoa的文章的更多相关文章
- ref:【干货分享】PHP漏洞挖掘——进阶篇
ref:http://blog.nsfocus.net/php-vulnerability-mining/ [干货分享]PHP漏洞挖掘——进阶篇 王陶然 从常见的PHP风险点告诉你如何进行PH ...
- 最强最全干货分享:Android开发书籍、教程、工具等
最全干货分享,本文收集整理了Android开发所需的书籍.教程.工具.资讯和周刊各种资源,它们能让你在Android开发之旅的各个阶段都受益. 入门<Learning Android(中文版)& ...
- 干货分享:SQLSERVER使用裸设备
干货分享:SQLSERVER使用裸设备 这篇文章也适合ORACLE DBA和MYSQL DBA 阅读 裸设备适用于Linux和Windows 在ORACLE和MYSQL里也是支持裸设备的!! 介绍 大 ...
- iOS - GitHub干货分享(APP引导页的高度集成 - DHGuidePageHUD - ②)
距上一篇博客"APP引导页的高度集成 - DHGuidePageHUD - ①"的发布有一段时间了, 后来又在SDK中补充了一些新的内容进去但是一直没来得及跟大家分享, 今天来跟大 ...
- 【干货分享】Node.js 中文资料导航
这篇文章与大家分享一批高质量的的 Node.js 中文资料.Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台, 用来方便地搭建快速的, 易于扩展的网络应用 Node ...
- 【腾讯Bugly干货分享】总结一个技术总监的教训和经验
本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com/s/Ry-G0Nikh6m-h3ZVC2cLyQ 导语 20 ...
- 关于Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇高质量的博文)
Java多线程的线程同步和线程通信的一些小问题(顺便分享几篇质量高的博文) 前言:在学习多线程时,遇到了一些问题,这里我将这些问题都分享出来,同时也分享了几篇其他博客主的博客,并且将我个人的理解也分享 ...
- 干货分享:QQ群排名霸屏优化规则靠前的新技术
谈起QQ群排名的优化规则,很多人又爱又恨,原因很简单,爱他的都是引流效果是非常好的,通过关键词搜索排名好的技术,能排到全国默认前三,叫人怎能不爱他,恨的原因也恨简单,无论你的群完善的再怎么好,好像都无 ...
- Unite 2017 干货整理 优化篇
Unite 2017 干货整理 优化篇 2017年05月16日 将Unite 2017的一些演讲做了整理. 本篇有内存,CPU.GC.UI.渲染性能指标.Tips几个小节. 内容持续整理中. 内存 ...
随机推荐
- Murano Weekly Meeting 2015.09.15
Meeting time: 2015.September.15th 1:00~2:00 Chairperson: Serg Melikyan, PTL from Mirantis Meeting s ...
- vs2012配置使用entity framework 6
项目中使用mysql作为数据库,想快速地实现一些数据服务,为了节省开发时间,提升开发效率,性能不是考虑的重点,所以选择了使用ORM框架:Entity Framework.指定了DB的table des ...
- 手机web前端调试页面的几种方式
前言 PC端web页面调试比较容易,这里主要说几种移动端调试的方法,从简单到复杂.从模拟调试到远程调试,大概分为几部分: 1.Chrome DevTools(谷歌浏览器)的模拟手机调试 2.weinr ...
- HDU 1281——棋盘游戏——————【最大匹配、枚举删点、邻接表方式】
棋盘游戏 Time Limit:1000MS Memory Limit:32768KB 64bit IO Format:%I64d & %I64u Submit Status ...
- AngularJS directive 不执行
检查下directive的命名,是不是含有特殊符号和大写,全部改为小写就ok: 原因:html不支持骆驼峰命名,只支持小写:
- git分支合并冲突
合并冲突 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们. 如果你对 #53 问题的修改和有关 hotfix 的修改都涉及到同一个文件的同一处,在合并 ...
- Vue.js基础语法(一)
vue学习的一系列,全部来自于表哥---表严肃,是我遇到过的讲课最通透,英文发音最好听的老师,想一起听课就去这里吧 https://biaoyansu.com/i/hzhj1206 前言: 前端解析数 ...
- 错误Cannot find module 'stylus'
vue项目中使用stylus预处理器写css语法,老是出现 Cannot find module ‘stylus’ 的错误,鼓捣了很久,包括webstorm中配置stylus的支持,安装依赖. 终于找 ...
- 将Android工程打成jar包之后对资源的调用。
Android工程不能完整的打包成jar包.这个主要是R文件导致的,但是我们可以将除了资源文件中的所有东西都打到jar包中.Activity.View等等类. 提供jar包的同时,还需要提供资源文件. ...
- SpringMVC快速入门
导入开发包 前6个是Spring的核心功能包[IOC],第7个是关于web的包,第8个是SpringMVC包 org.springframework.context-3.0.5.RELEASE.jar ...