写在前面
MVC,MVP,MVVM……移动端的开发可谓是在MVX的海洋中摸爬滚打!然而,V和M的概念不说,关于P,关于VM,它为什么叫Presenter,为什么叫ViewModel?我们实践中的P,VM所做的事情真的和它们的概念对得上么?
本篇即基于MVVM在移动端的应用这一话题做一些简单的讨论,希望大家可以借以回顾自己搭过的框架,码过的代码,能唤起一些有趣更有意义的思考!
第一篇:我对“MVVM”的初识
那是在2015年的时候,MVVM被炒的火热,一日,面朝我已经尽心竭力做好了概念分组的“巨型VC”,我无力地将头专向小马哥,“编辑这部分代码我还是感到很难受……”
//某VC中
[aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
[Error check code];
NSDictionary *tmpDic = (NSDictionary *)respData;
NSNumber *tmpNumber = [tmpDic objectForKey:@"boolVal"] ;
if (YES == [tmpNumber boolValue]) {
_contentView.titleLable.text = [tmpDic objectForKey:@"title"] ;
} else {
_contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
}}];
如上,很常见的,网络请求后,错误检查,解析数据并用数据更新视图。
“如此清晰顺畅无比的场景,还能如何优化呢?‘感到难受’,这也没办法啦,什么都不能做了!”我们有这样自我合理化的内心os太正常不过了,但多年的经验不断地让我印证一个真理:事出反常必有鬼(感觉不爽,必可优化)。果然!……
“那是因为你把视图和逻辑耦合到一起了!”小马哥回答。
Step1 可以将我们期望在回调中做的事情进行简单的概念分组
//某VC中
[aRequester sendPostReqWithUrl:aUrl paras:paraDic response:^(id respData, NSError *error) {
/* Error check */
/* Handle Parser */
/* Update View */
}];
Step2 分别抽象出解析&更新视图的具体处理
/* Handle Parser */
- (NSString *)parserTitle:(NSDictionary *respData) {
NSString *tmpStr = nil;
NSNumber *tmpNumber = [respData objectForKey:@"boolVal"] ;
if (YES == [tmpNumber boolValue]) {
tmpStr = [respData objectForKey:@"title"] ;
} else {
tmpStr = [respData objectForKey:@"title2"] ;
}}];
return tmpStr;
}
/* Update View */
- (void)updateViewWithTitle:(NSString *)title {
_contentView.titleLable.text = title;
}
Step3 方法抽象封装,并新建文件(logicModel)来承载功能简单但实现复杂的逻辑代码
//某VC中
[_logicModel requestTitleInfoWithResponse:^(NSString *title) {
/* Update View */
}];
//封装到logicModel文件中
- (void)requestUserInfoWithResponse:(void(^)(id userInfo, NSError *error))callback {
[aRequester sendPostReqWithUrl:(NSString *)url paras:(NSDictionary *)paras response:(id respData, NSError *error) {
/* Error check */
/* Handle parser */
/* Callback */
callback(title);
}];
}
有什么不一样么?或许不经过实践操作中对代码维护效率精益求精地追求,很难通过上面的例子直观的理解抽象的好处。甚至会有一些浮躁的逆反心理作祟,认为这么做多此一举。
抽象/封装不一定都是好的,它们的应用要权衡地考虑某个实现模块的复杂性,从而选择一个最合适的抽象层次。而抽象/封装的最基本原则参考,我想应当是:概念合理。
上面的例子即是简单地将“数据的处理”和“视图的更新”相互独立起来,使视图的更新更为纯粹!简言之,我们期望避免下面场景的出现
if (YES == [tmpNumber boolValue]) {
_contentView.titleLable.text = [tmpDic objectForKey:@"title"] ;
} else {
_contentView.titleLable.text = [tmpDic objectForKey:@"title2"] ;
}
然后,小马哥告诉我,这就是MVVM,即当前最流行的Model-View-ViewModel模式。
我上面起名为LogicModel的文件即为小马哥所说的“VM”,它负责将网络请求、请求解析和一些数据处理逻辑进行封装,从而使VC变得“轻”一些。
想象一下我们的思维走向:
1) 与视图无关的数据逻辑问题,直接往从VM入手排查
2) 与视图展示有关的逻辑问题,在VC到View的流程中一看数据对接,二看VM反馈的数据是否有误。
可谓是结点清晰,定位问题毫无压力!大赞!不愧是“MVVM”!
可是,VM就是逻辑封装自然演化的一个“代号”么?
VM = View Model,是视图的模型,“模型”一词,从概念上倾向于一种“静态”,而逻辑处理,网络请求,信号接收这些趋向于一种异步的“动态”,而且好像和“视图”的概念差别有些大。这种封装固然有它的优势所在,但MVVM的设计者干嘛对它起名为VM呢?视图的模型?叫LogicModel (逻辑模型),或者VCTool(VC工具)怎么都比VM合理吧?
只是一个名字而已嘛!然而,就我对“大牛”的理解,他们对于某种概念的名称拟定,是绝不会马马虎虎了事的!
VM的本源必然就是一个VM!一个视图的模型!
第二篇:追溯MVVM的提出
或许是我对于VM的理解方向不对吧?毕竟从网上的众多文章的分析说明,从同事的实践中,我们对于MVVM又或MVP的在移动端的应用实践竟然出奇地一致!(如下图)
image.png
翻阅了几十篇相关的文章,每篇的说的颇有道理,很多文章还是分了上中下篇,并配以图示,似颇为系统的对MVC,MVP,MVVM进行介绍。不得不说,这些文章颇具指导意义,确实可以让很多限于逻辑耦合深渊的朋友找到一盏明灯,让他们的项目变得清晰而易于维护。但我还是任性地感觉,他们在打着MVVM的旗号在讲VC减负——我想要做的,是接近MVVM的本源!
MVVM 最早于 2005 年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并且应用在微软的软件开发中。我找到了那片博文,并进行了翻译和仔细的思考探究。
《Model/View/ViewModel pattern for building WPF apps》
John Gossman
译文链接:
https://www.jianshu.com/p/b0b80163782f
原文链接: https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/
这篇博文中,有这样这样两句有趣的话:
1)Model/View/ViewModel is a variation of Model/View/Controller (MVC) that is tailored for modern UI development platforms where the View is the responsibility of a designer rather than a classic developer.(译:MVVM是MVC模式的一个演变,针对一个视图的展示样式,比起传统的开发者,现在往往是设计师更为关注,MVVM正是为这种状况而定制的一种模式。)
2)The term means "Model of a View", and can be thought of as abstraction of the view。(译:它意为“视图的模型”,可以将它想象成一个抽像化的视图。)
那么,基于对MVVM本源的解读,我们在一个“可以被想象成视图的抽象”的VM中添加大量网络,页面跳转等逻辑显然不太合适了。(它是视图,它是视图,它是视图,请这样对自己洗脑!)同时,我们也想思考下关于“让设计师去完成视图展示”这个有趣的点。
第三篇:MVVM基于WPF的应用(最初的应用场景)
WPF即Windows Presentation Foundation,是微软推出的基于Windows 的用户界面框架。
第二篇的译文中原作者举例的应用界面是Sparkle,我这边则以类似的OmniGraffle(一款原型绘制软件)的界面进行说明。(没什么特别的原因,因为我正在用OmniGraffle,更容易截图:)
image.png如果大家阅读了第二篇提供的《Model/View/ViewModel pattern for building WPF apps》,你会发现文中举例的Sparkle界面操作栏和我举例的OmniGraffle界面圈红的部分是很相似的。那么,参照文中的VM划分方式,我们可以设计A部分对应一个ViewModel A(当然OmniGraffle不一定是这样实现的),B部分对应一个ViewModel B,然后B部分的“填充”,“笔画”,“阴影”,“形状”,“线条”亦可以分别对应5个小的ViewModel……(如下图)
image.png一个有趣的点,View的层次叠加变成了VM的层次叠加!VM真如一个View的抽象一般!
同时,第二篇摘录的另一段译文引导的另一个问题:什么叫设计师更关注UI的页面展示?不要小看我们UI同学哦,当下很多的设计师都有css、html的开发经验,同时MVVM由微软提出,记得微软有一个自己的XAML吧?它正式一种搭建UI的语言。所以,基于MVVM的模式,我们至少可以从概念上将视图完全剥离(甚至交给UI同学去渲染与实现),模型中只要有视图中需要展示的元素的具体内容数据即可,他不关心任何视图的布局,渲染效果。
同事,针对视图的布局和效果的动态改变,我们将这些改变抽象成状态,存放在VM当中。至此,一个最最简单的MVVM元组得以实现。
image.png第四篇:MVVM基于APP的应用
回到市面上较为流行的一种类“MVVM实践模式”,它们以“MVC+VC减负+概念抽象封装”作为基本的思路参考,让VC作为VM和View沟通的主桥梁。
如下图,一般是一个VC包含一个VM和一个主View,然后VM或许会处理少量“双向绑定的任务”,同时也可能将更多的比如网络答复的操作动作回调给VC去处理视图更新
image.png这种模式易于理解也确实可以实际的提高代码的概念性和可维护性。但我们发现,视图的更新走了两条长线:
1) 介由VM的绑定实现模型更新视图;
2) 借由VM的回调实现VC控制更新视图。
这总让我们感到不够清爽:当我希望将视图中的一段文字由“我的领导是个坏人”改为“我的领导是个好人”时候,没有明确的概念告诉我哪一条“线”是有决策力的“线”(可以成功进行修改的线)。
“看代码不就知道了?”
请记住:
1 有思想的代码几乎不需要透过代码来定位问题
2 维护代价的“积累”不是“叠加”而是“逻辑分支的叠乘”(每一次“选线”的犹豫,都是一层逻辑分支)
所以,看代码当然可以解决问题!甚至针对复杂的工程,你大可花费一个月将它的每个细节流程完全理透!然后心满意足的大赞自我的耐心和代码阅读能力!不想,领导已经看到了那句你还没有来得及改掉的“我的领导是个坏蛋”……
言归正传,第三篇我们基于MVVM在WPF中的应用分析貌似还蛮顺畅的,但好像应用再APP中,有什么地方有些……怪!根源在哪里?或许如下几个问题可以作为我们的参考:
1) VC的地位到底更倾向于什么?是V?是C?是VM?
2) 网络请求/视图生命周期/路由跳转这些在APP端大量出现的概念模块,它们在MVVM中有着怎样的概念归属?
4.1 MVVM基于APP的基础架构&模块分工
我们尝试一下下面的这套交互结构
image.png首先我们明确一个点,在一个MVC结构中,即便抛开视图后,模型和控制器处理的大部分业务逻辑,都是为视图服务的。我们常说的“重VC”,很大一部分重在视图相关的逻辑或是为之服务的逻辑。
所以,当我们抽象一些视图的基础模型,并通过VM将视图本身的(不需要与外界交互的)状态变迁逻辑封装在一个MVVM组的内部,对外(对VC)只暴露必要的数据更新和消息回调接口。繁琐的视图逻辑就可以被限制在一个MVVM当中(它确实也应当在那里)。这时留在VC中的逻辑,一般情况下就很少了。如果此刻的VC还让你感到“重”的话,我们大可再对其抽象一个VC-Logic,将复杂的逻辑进行封装。
各个模块所负责的主要工作可以参考下图
image.png如图,VC中的“生命周期控制”,“网络请求”,“路由”,View中的“视图布局”,“控件效果”都很好理解,让人一眼摸不清的概念主要存在所谓的VM当中,我们来简单说明:
1)什么是“处理视图状态”?
视图可能根据不同的状态有不同的展示内容,甚至展示效果。我们常见的“cur”(current)前缀就适用于说明这种场景。“当前选择的模块”,“某个按钮当前的选择状态”,这些表示视图状态的操作变量的定义应当在VM当中,相关的逻辑交互也应当在VM当中。如果说View提供了视图的所有展示元素;那么VM则可以确定某个视图模块某一时刻某一个状态下的呈现内容。
2)什么是“处理视图协作”?
一个VM不一定只和一个View存在关联,它可能同时协调多个视图。
我们以同程旅行的一个筛选界面作为参考场景进行说明:
当我们将“4.5分以上”后面的对号勾上的时候,上面的“4.5分以上”会被同步勾取,同时,“评分”后面会多出个小绿点,这表示评分这页的筛选条件选择的不是默认的“不限”。很显然,关键词模块、筛选分类模块、筛选详情模块正常人都会分成3部分视图绘制。这三个视图间显然是有交互关系的(即“筛选详情模块”的勾选触发了“关键词模块”的高亮和“筛选分类模块”的加点),而VM即是处理这种交互关系理想场所。
3)什么是“数据绑定”?
这边特指将一个模型数据和视图中的一个展示内容进行关联绑定;
单向绑定一般指模型数据变化触发对应的视图数据变化
双向绑定指模型数据,视图数据任意一方变化,都会触发另一方的同步变化。
4)什么是“数据转换”?
我们不能企望所有的模型数据都能直接被视图使用,比如模型中是一个BOOL(0/1)值,而对应的视图展示期望为“是”/“否”,类似这样的数据转化工作,交给VM吧!
4.2 MVVM基于APP的抽象讨论
我们再来讨论一下几个观点的理解:
1)VC是特殊的VM
很常见的,VC中除了主要的展示视图外,还有一个导航条(NavigationBar),而我们又很常见导航条要根据主视图的滚动而改变展示效果(比如随着视图滚动变得透明),这种视图的交互显然只能在VC中处理。这很正常,VC可以理解为特殊的VM,即它会负责一些类似VM的协调工作(协调本身也是C的职责),亦会负责VC的其他本职工作(如控制视图生命周期等)。
2)VM的是可以存在类似View的层次的
写视图,Subview(子视图)的概念是逃不掉的,而参照“将VM理解为视图”的思路,复杂视图中,VM的层次也是逃不掉的,像图中一样。
大家会发现,我在VC下面标明了“mainVM”,在一些MVVM下面标明了“mini”,mainVM好理解,因为前面我们已经引入了“VC是特殊的VM这一思路”,但是mini呢?
大多数场景,我们一个页面的视图交互不会特别复杂,所以,一般的多层视图,只用一个VM管理就够了。但有时我们会希望对视图层中的小模块进行MVVM封装,因为它是“通用”的(希望被复用的),通用的小视图模块往往是“简单”的,这是mini的第一层含义。
同时,当我们没有引入VM概念的时候,View就单纯地是View么?想想UIButton吧,可以设定选择状态不说,它还可以随时获取当前按钮的选择状态(selected),这不就是说UIButton保存了视图状态么!如果把UIButton进行细致的概念拆分,不就变成了我们的MVVM组么!所以,我们很多系统的视图控件,本来就可以理解为mini的MVVM。
VM从某种角度上讲,就是一个视图!
第五篇:RAC对iOS实践MVVM的价值
很显然,上面的讲述中,我们只字未提到RAC(ReactiveCocoa),所以,RAC本身是和MVVM没有本质上的关联的。但无可反驳的是,使用RAC确实能让MVVM的实践上显得更加精巧。
5.1 快速绑定
我们在应用中运用的视图更新接口,block回调,代理,通知,KVO,目标动作对……都可以理解为广义“绑定”所依赖的技巧,RAC则将上述机制统一成“消息”,可以让我们以更简单的方式处理绑定动作。请看下面的例子。
1)常规方式:双向绑定一个字符串和一个textField的text值
/* 1. 使textField中的text改变时,字符串textStr可以同步变化 */
[_textField addTarget:self action:@selector(valueChanged:) forControlEvents:UIControlEventEditingChanged];
- (void)valueChanged:(UITextField *)textField {
_textStr = _textField.text;
}
/* 2. 使textStr改变时,textField中的text可以同步变化 */
[self addObserver:self
forKeyPath:@"textStr"
options:NSKeyValueObservingOptionNew
context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (object == self && [keyPath isEqualToString:@"textStr"]) {
_textField.text = _textStr;
}
}
2)RAC方式:双向绑定一个字符串和一个textField的text值
/* 1. 使textField中的text改变时,字符串textStr可以同步变化 */
RAC(self, textStr) = _textField.rac_textSignal;
/* 2. 使textStr改变时,textField中的text可以同步变化 */
RAC(self.textField, text) = RACObserve(self, textStr);
代码的简化是显而易见的。
5.2 多元监听
iOS中对于代理的应用场景还是很多的。而基本的代理模式中,某个模块的代理者只能有一个。为了让多个对象同时接收代理消息,我们不得不修改模块结构,又或者自定制一个自以为很简单完美的代理队列,又或将代理改用通知?!(不想玩死自己的话,放弃在局部使用这种思路吧!)甚至,还有更奇葩的设计。
然而,在RAC中很简单。
下面的代码即实现了textStr和textStr2同时监听textField的text的变化
(处理代理一样的简单,因为RAC全部将其抽象成为了“消息”)
RAC(self, textStr) = _textField.rac_textSignal;
RAC(self, textStr2) = _textField.rac_textSignal;
但是,应用中的意义呢?
将我们封装的VM可以理解为一个模块,对一个模块而言,没什么比输入输出接口的设计更加重要了。而互联网时代,神奇的需求变动在很多场景下让我们不得不对模块进行更新,甚至更新模块的对外接口。多元监听的支持可以大大降低模块对外接口更新的复杂性。(接口的更新很容易牵连整个模块的基础框架,更细节的分析在此不再赘述)
5.3 元组的引入
元组,并不是一个让人感到陌生的概念,它即代表一组约定的有序数据,该组数据中每个数据的数据结构不需要统一。
在MVVM中(其实普通的视图设计中也是),为了方便视图的展示,我们常常要约定一些轻量级的纯视图数据结构。这时候Tuple或许会是最契合我们场景的概念。
为什么?
1)Tuple的概念定位不同于Array,tuple的长度一般是确定的,tuple组内每个元素的类型不要求一致。
2)Tuple的概念亦不同于Dictionary,tuple无须将一个数组分为key,value两个部分(繁琐,麻烦~),同时,tuple是有序的(字典是无序的)。
当然,介于tuple的灵活性特征,使用场景一定要控制在小范围,需要定义对象的时候,还是要定义的!万万不可将tuple在不可控的大范围使用。(一样是玩死自己的行为)。
5.4 没有RAC不能应用MVVM?
我不这么认为:
1)如我们之前说过的,MVVM与RAC没有本质的关联;
2)如5.1~5.3,RAC可以使我们应用MVVM的一些场景变得更为简单优雅,RAC针对MVVM优化的问题在我们不使用MVVM时依然也存在(你用MVC是有些场景一样要用KVO),而且,这些场景不算是决定性的(双向绑定的实践应用场景其实很少)
结语
这篇文章的准备在一个月前,中间因为主工作项目原因有各种间断和搁置,但也庆幸有这样的项目需求,可以让我将其中部分的思路得以实践和印证。相信这篇从MVVM的提出为出发点,经过了反复思考印证,将模块的概念分工多次推倒重组的文章,可以真正为大家对MVVM理解上提供有价值的思路参考,为实践中遇到的一些让人感到不舒服的代码的优化方向上提供有价值的思路参考,为MVVM在移动端的应用实践提供有价值的思路参考!
网友评论
个人感觉这不是绝对的,比如:1,你可以选择VM继承于NSObject,然后提供一个对应View的对外属性以供调用者操作视图;2,你也可以选择VM继承于UIView,相当于这个VM直接就是一个View的载体;3,你更可以隐藏VM的存在,就像UIButton,UISwitch一样,它们的state又或on属性本质也是应该VM处理的“状态属性”。
所以,本质上,结合不同的场景,选择的实践是灵活的。简单的视图大可像系统控件一样直接隐藏VM的存在;复杂的视图,你会感受到“视图布局”&“视图逻辑”甚至还有一些“数据转化逻辑”冗杂在一起所造成的【每次看代码都要回忆一下之前是怎么想的甚至完全回忆不起来】的痛苦了。自然而然就会选择逻辑概念的抽象与隔离,这时候——VM的思路借鉴自然会发挥它的价值。