iOS应用架构谈 view层的组织和调用方案分为两篇
《iOS应用架构谈 开篇》出来之后,很多人来催我赶紧出第二篇。这一篇文章出得相当艰难,因为公司里的破事儿特别多,我自己又有点私事儿,以至于能用来写博客的时间不够充分。
当我们开始设计View
层的架构时,往往是这个App
还没有开始开发,或者这个App已经发过几个版本了,然后此时需要做非常彻底的重构。
一般也就是这两种时机会去做View层架构,基于这个时机的特殊性,我们在这时候必须清楚认识到:View层的架构一旦实现或定型,在App发版后可修改的余地就已经非常之小了。因为它跟业务关联最为紧密,所以哪怕稍微动一点点,它所引发的蝴蝶效应都不见得是业务方能够hold住的。这样的情况,就要求我们在实现这个架构时,代码必须得改得勤快,不能偷懒。也必须抱着充分的自我怀疑态度,做决策时要拿捏好尺度。
View层的架构非常之重要,在我看来,这部分架构是这系列文章涉及4个方面最重要的一部分,没有之一。为什么这么说?
View层架构是影响业务方迭代周期的因素之一
产品经理产生需求的速度会非常快,尤其是公司此时仍处于创业初期,在规模稍大的公司里面,产品经理也喜欢挖大坑来在leader
面前刷存在感,比如阿里。这就导致业务工程师任务非常繁重。正常情况下让产品经理砍需求是不太可能的,因此作为架构师,在架构里有一些可做可不做的事情,最好还是能做就做掉,不要偷懒。这可以帮业务方减负,编写代码的时候也能更加关注业务。
我跟一些朋友交流的时候,他们都会或多或少地抱怨自己的团队迭代速度不够快,或者说,迭代速度不合理地慢。我认为迭代速度不是想提就能提的,迭代速度的影响因素有很多,一期PRD里的任务量和任务复杂度都会影响迭代周期能达到什么样的程度。抛开这些外在的不谈,从内在可能导致迭代周期达不到合理的速度的原因来看,其中有一个原因很有可能就是View层架构没有做好,让业务工程师完成一个不算复杂的需求时,需要处理太多额外的事情。当然,开会多,工程师水平烂也属于迭代速度提不上去的内部原因,但这个不属于本文讨论范围。还有,加班不是优化迭代周期的正确方式
,嗯。
一般来说,一个不够好的View层架构,主要原因有以下五种:
- 代码混乱不规范
- 过多继承导致的复杂依赖关系
- 模块化程度不够高,组件粒度不够细
- 横向依赖
- 架构设计失去传承
这五个地方会影响业务工程师实现需求的效率,进而拖慢迭代周期。View架构的其他缺陷也会或多或少地产生影响,但在我看来这里五个是比较重要的影响因素。如果大家觉得还有什么因素比这四个更高的,可以在评论区提出来我补上去。
对于第五点我想做一下强调:架构的设计是一定需要有传承的,有传承的架构从整体上看会非常协调。但实际情况有可能是一个人走了,另一个顶上,即便任务交接得再完整,都不可避免不同的人有不同的架构思路,从而导致整个架构的流畅程度受到影响。要解决这个问题,一方面要尽量避免单点问题,让架构师做架构的时候再带一个人。另一方面,架构要设计得尽量简单,平缓接手人的学习曲线。我离开安居客的时候,做过保证:凡是从我手里出来的代码,终身保修
。所以不要想着离职了就什么事儿都不管了,这不光是职业素养问题,还有一个是你对你的代码是否足够自信的问题。传承性对于View层架构非常重要,因为它距离业务最近,改动余地最小。
所以当各位CTO、技术总监、TeamLeader们觉得迭代周期不够快时,你可以先不忙着急吼吼地去招新人,《人月神话》早就说过加人不能完全解决问题。这时候如果你可以回过头来看一下是不是View层架构不合理,把这个弄好也是优化迭代周期的手段之一。
View层架构是最贴近业务的底层架构
View层架构虽然也算底层,但还没那么底层,它跟业务的对接面最广,影响业务层代码的程度也最深。在所有的底层都牵一发的时候,在View架构上牵一发导致业务层动全身的面积最大。
所以View架构在所有架构中一旦定型,可修改的空间就最小,我们在一开始考虑View相关架构时,不光要实现功能,还要考虑更多规范上的东西。制定规范的目的一方面是防止业务工程师的代码腐蚀View架构,另一方面也是为了能够有所传承。按照规范来,总还是不那么容易出差池的。
还有就是,架构师一开始考虑的东西也会有很多,不可能在第一版就把它们全部实现,对于一个尚未发版的App来说,第一版架构往往是最小完整功能集,那么在第二版第三版的发展过程中,架构的迭代任务就很有可能不只是你一个人的事情了,相信你一个人也不见得能搞定全部。所以你要跟你的合作者们有所约定。另外,第一版出去之后,业务工程师在使用过程中也会产生很多修改意见,哪些意见是合理的,哪些意见是不合理的,也要通过事先约定的规范来进行筛选,最终决定如何采纳。
规范也不是一成不变的,什么时候枪毙意见,什么时候改规范,这就要靠各位的技术和经验了。
这篇文章讲什么?
- View代码结构的规定
- 关于view的布局
- 何时使用storyboard,何时使用nib,何时使用代码写View
- 是否有必要让业务方统一派生ViewController?
- 方便View布局的小工具
- MVC、MVVM、MVCS、VIPER
- 本门心法
- 跨业务时View的处理
- 留给评论区各种补
- 总结
View代码结构的规定
架构师不是写SDK出来交付业务方使用就没事儿了的,每家公司一定都有一套代码规范,架构师的职责也包括定义代码规范。按照道理来讲,定代码规范应该是属于通识,放在这里讲的原因只是因为我这边需要为View添加一个规范。
制定代码规范严格来讲不属于View层架构的事情,但它对View层架构未来的影响会比较大,也是属于架构师在设计View层架构时需要考虑的事情。制定View层规范的重要性在于:
- 提高业务方View层的可读性可维护性
- 防止业务代码对架构产生腐蚀
- 确保传承
- 保持架构发展的方向不轻易被不合理的意见所左右
在这一节里面我不打算从头开始定义一套规范,苹果有一套Coding Guidelines,当我们定代码结构或规范的时候,首先一定要符合这个规范。
然后,相信大家各自公司里面也都有一套自己的规范,具体怎么个规范法其实也是根据各位架构师的经验而定,我这边只是建议各位在各自规范的基础上再加上下面这一点。
viewController的代码应该差不多是这样:
[站外图片上传中...(image-88be53-1555766670103)]
所有的属性都使用getter和setter
不要在viewDidLoad里面初始化你的view然后再add,这样代码就很难看。在viewDidload里面只做addSubview的事情,然后在viewWillAppear里面做布局的事情(勘误1),最后在viewDidAppear里面做Notification的监听之类的事情。至于属性的初始化,则交给getter去做。
比如这样:
#pragma mark - life cycle
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.firstTableView];
[self.view addSubview:self.secondTableView];
[self.view addSubview:self.firstFilterLabel];
[self.view addSubview:self.secondFilterLabel];
[self.view addSubview:self.cleanButton];
[self.view addSubview:self.originImageView];
[self.view addSubview:self.processedImageView];
[self.view addSubview:self.activityIndicator];
[self.view addSubview:self.takeImageButton];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
CGFloat width = (self.view.width - 30) / 2.0f;
self.originImageView.size = CGSizeMake(width, width);
[self.originImageView topInContainer:70 shouldResize:NO];
[self.originImageView leftInContainer:10 shouldResize:NO];
self.processedImageView.size = CGSizeMake(width, width);
[self.processedImageView right:10 FromView:self.originImageView];
[self.processedImageView topEqualToView:self.originImageView];
CGFloat labelWidth = self.view.width - 100;
self.firstFilterLabel.size = CGSizeMake(labelWidth, 20);
[self.firstFilterLabel leftInContainer:10 shouldResize:NO];
[self.firstFilterLabel top:10 FromView:self.originImageView];
... ...
}
这样即便在属性非常多的情况下,还是能够保持代码整齐,view的初始化都交给getter
去做了。总之就是尽量不要出现以下的情况:
- (void)viewDidLoad
{
[super viewDidLoad];
self.textLabel = [[UILabel alloc] init];
self.textLabel.textColor = [UIColor blackColor];
self.textLabel ... ...
self.textLabel ... ...
self.textLabel ... ...
[self.view addSubview:self.textLabel];
}
这种做法就不够干净,都扔到getter里面去就好了。关于这个做法,在唐巧的技术博客里面有一篇文章和我所提倡的做法不同,这个我会放在后面详细论述。
getter和setter全部都放在最后
因为一个ViewController
很有可能会有非常多的view
,就像上面给出的代码样例一样,如果getter
和setter
写在前面,就会把主要逻辑扯到后面去,其他人看的时候就要先划过一长串getter和setter,这样不太好。然后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle
,然后是Delegate
方法实现,然后是event response
,然后才是getters and setters
。这样后来者阅读代码时就能省力很多。
每一个delegate都把对应的protocol名字带上,delegate方法不要到处乱写,写到一块区域里面去
比如UITableViewDelegate的方法集就老老实实写上#pragma mark - UITableViewDelegate
。这样有个好处就是,当其他人阅读一个他并不熟悉的Delegate
实现方法时,他只要按住command
然后去点这个protocol
名字,Xcode就能够立刻跳转到对应这个Delegate
的protocol
定义的那部分代码去,就省得他到处找了。
event response专门开一个代码区域
所有button
、gestureRecognizer
的响应事件都放在这个区域里面,不要到处乱放。
关于private methods,正常情况下ViewController里面不应该写
不是delegate
方法的,不是event response
方法的,不是life cycle
方法的,就是private method
了。对的,正常情况下ViewController
里面一般是不会存在private methods
的,这个private methods一般是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他做成一个模块,哪怕这个模块只有一个函数也行。
ViewController
基本上是大部分业务的载体,本身代码已经相当复杂,所以跟业务关联不大的东西能不放在ViewController
里面就不要放。另外一点,这个private method
的功能这时候只是你用得到,但是将来说不定别的地方也会用到,一开始就独立出来,有利于将来的代码复用。
为什么要这样要求?
我见过无数ViewController
,代码布局乱得一塌糊涂,这里一个delegate
那里一个getter
,然后ViewController
的代码一般都死长死长的,看了就让人头疼。
定义好这个规范,就能使得ViewController
条理清晰,业务方程序员很能够区分哪些放在ViewController
里面比较合适,哪些不合适。另外,也可以提高代码的可维护性和可读性。
关于View的布局
业务工程师在写View
的时候一定逃不掉的就是这个命题。用Frame
也好用Autolayout
也好,如果没有精心设计过,布局部分一定惨不忍睹。
直接使用CGRectMake
的话可读性很差,光看那几个数字,也无法知道view
和view
之间的位置关系。用Autolayout
可读性稍微好点儿,但生成Constraint
的长度实在太长,代码观感不太好。
Autolayout
这边可以考虑使用Masonry
,代码的可读性就能好很多。如果还有使用Frame
的,可以考虑一下使用这个项目。
这个项目里面提供了Frame
相关的方便方法(UIView+LayoutMethods)
,里面的方法也基本涵盖了所有布局的需求,可读性非常好,使用它之后基本可以和CGRectMake
说再见了。因为天猫在最近才切换到支持iOS6,所以之前天猫都是用Frame
布局的,在天猫App
中,首页,范儿部分页面的布局就使用了这些方法。使用这些方便方法能起到事半功倍的效果。
这个项目也提供了Autolayout
方案下生产Constraints
的方便方法(UIView+AEBHandyAutoLayout)
,可读性比原生好很多。我当时在写这系列方法的时候还不知道有Masonry
。知道有Masonry
之后我特地去看了一下,发现Masonry
功能果然强大。不过这系列方法虽然没有Masonry
那么强大,但是也够用了。当时安居客iPad版App全部都是Autolayout
来做的View
布局,就是使用的这个项目里面的方法。可读性很好。
让业务工程师使用良好的工具来做View的布局,能提高他们的工作效率,也能减少bug发生的几率。架构师不光要关心那些高大上的内容,也要多给业务工程师提供方便易用的小工具,才能发挥架构师的价值。
何时使用storyboard,何时使用nib,何时使用代码写View
这个问题唐巧的博客里这篇文章也提到过,我的意见和他是基本一致的。
在这里我还想补充一些内容:
具有一定规模的团队化iOS开发(10人以上)有以下几个特点:
- 同一份代码文件的作者会有很多,不同作者同时修改同一份代码的情况也不少见。因此,使用Git进行代码版本管理时出现Conflict的几率也比较大。
- 需求变化非常频繁,产品经理一时一个主意,为了完成需求而针对现有代码进行微调的情况,以及针对现有代码的部分复用的情况也比较多。
- 复杂界面元素、复杂动画场景的开发任务比较多。
如果这三个特点你一看就明白了,下面的解释就可以不用看了。如果你针对我的倾向愿意进一步讨论的,可以先看我下面的解释,看完再说。
同一份代码文件的作者会有很多,不同作者同时修改同一份代码的情况也不少见。因此,使用Git进行代码版本管理时出现Conflict的几率也比较大。
iOS开发过程中,会遇到最蛋疼的两种Conflict
一个是project.pbxproj,另外一个就是StoryBoard或XIB。因为这些文件的内容的可读性非常差,虽然苹果在XCode5(现在我有点不确定是不是这个版本了)中对StoryBoard的文件描述方式做了一定的优化,但只是把可读性从非常差提升为很差。
然而在StoryBoard
中往往包含了多个页面,这些页面基本上不太可能都由一个人去完成,如果另一个人在做StoryBoard
的操作的时候,出于某些目的动了一下不属于他的那个页面,比如为了美观调整了一下位置。然后另外一个人也因为要添加一个页面,而在Storyboard
中调整了一下某个其他页面的位置。那么针对这个情况我除了说个呵呵以外,我就只能说:祝你好运。看清楚哦,这还没动具体的页页面内容呢。
但如果使用代码绘制View
,Conflict
一样会发生,但是这种Conflict
就好解很多了,你懂的。
需求变化非常频繁,产品经理一时一个主意,为了完成需求而针对现有代码进行微调的情况,以及针对现有代码的部分复用的情况也比较多。
我觉得产品经理一时一个主意不是他的错,他说不定也是被逼的,比如谁都会来掺和一下产品的设计,公司里的所有人,上至CEO,下至基层员工都有可能对产品设计评头论足,只要他个人有个地方用得不爽(极大可能是个人喜好)然后又正好跟产品经理比较熟悉能够搭得上话,都会提出各种意见。产品经理躲不起也惹不起,有时也是没办法,嗯。
另外,如果出现部分的代码复用,比如说某页面下某个View也希望放在另外一个页面里,相关的操作就不是复制粘贴这么简单了,你还得重新link一遍。也很影响心情。
复杂界面元素,复杂动画交互场景的开发任务比较多。
要是想在基于StoryBoard
的项目中做一个动画,很烦。做几个复杂界面元素,也很烦。有的时候我们挂Custom View
上去,其实在StoryBoard
里面看来就是一个空白View。然后另外一点就是,当你的layout
出现问题需要调整的时候,还是挺难找到问题所在的,尤其是在复杂界面元素的情况下。
所以在针对View
层这边的要求时,我也是建议不要用StoryBoard
。实现简单的东西,用Code
一样简单,实现复杂的东西,Code
比StoryBoard
更简单。所以我更加提倡用code
去画view
而不是storyboard
。
是否有必要让业务方统一派生ViewController
有的时候我们出于记录用户操作行为数据的需要,或者统一配置页面的目的,会从UIViewController
里面派生一个自己的ViewController
,来执行一些通用逻辑。比如天猫客户端要求所有的ViewController
都要继承自TMViewController
。这个统一的父类里面针对一个ViewController
的所有生命周期都做了一些设置,至于这里都有哪些设置对于本篇文章来说并不重要。在这里我想讨论的是,在设计View架构时,如果为了能够达到统一设置或执行统一逻辑的目的,使用派生的手段是有必要的吗?
我觉得没有必要,为什么没有必要?
- 使用派生比不使用派生更容易增加业务方的使用成本
- 不使用派生手段一样也能达到统一设置的目的
这两条原因是我认为没有必要使用派生手段的理由,如果两条理由你都心领神会,那么下面的就可以不用看了。如果你还有点疑惑,请看下面我来详细讲一下原因。
为什么使用了派生,业务方的使用成本会提升?
其实不光是业务方的使用成本,架构的维护成本也会上升。那么具体的成本都来自于哪里呢?
-
1: 集成成本
这里讲的集成成本是这样的:如果业务方自己开了一个独立demo
,快速完成了某个独立流程,现在他想把这个现有流程集合进去。那么问题就来了,他需要把所有独立的UIViewController
改变成TMViewController。那为什么不是一开始就立刻使用TMViewController
呢?因为要想引入TMViewController
,就要引入整个天猫App
所有的业务线,所有的基础库,因为这个父类里面涉及很多天猫环境才有的内容,所谓拔出萝卜带出泥,你要是想简单继承一下就能搞定的事情,搭环境就要搞半天,然后这个小Demo
才能跑得起来。对于业务层存在的所有父类来说,它们是很容易跟项目中的其他代码纠缠不清的,这使得业务方开发时遇到一个两难问题:要么把所有依赖全部搞定,然后基于App环境(比如天猫)下开发Demo,要么就是自己Demo写好之后,按照环境要求改代码。这里的两难问题都会带来成本,都会影响业务方的迭代进度。
我不确定各位所在公司是否会有这样的情况,但我可以在这里给大家举一个我在阿里的真实的例子:我最近在开发某滤镜Demo和相关页面流程,最终是要合并到天猫这个App里面去的。使用天猫环境进行开发的话,
pod install
完所有依赖差不多需要10分钟,然后打开workspace
之后,差不多要再等待1分钟让xcode做好索引,然后才能正式开始工作。在这里要感谢一下则平,因为他在此基础上做了很多优化,使得这个1分钟已经比原来的时间短很多了。但如果天猫环境有更新,你就要再重复一次上面的流程,否则 就很有可能编译不过。拜托,我只是想做个Demo而已,不想搞那么复杂。
2: 上手接受成本
新来的业务工程师有的时候不见得都记得每一个ViewController
都必须要派生自TMViewController
而不是直接的UIViewController
。新来的工程师他不能直接按照苹果原生的做法去做事情,他需要额外学习,比如说:所有的ViewController
都必须继承自TMViewController
。3: 架构的维护难度
尽可能少地使用继承能提高项目的可维护性,具体内容我在《跳出面向对象思想(一) 继承》里面说了,在这里我想偷懒不想把那篇文章里说过的东西再说一遍。
其实对于业务方来说,主要还是第一个集成成本比较蛋疼,因为这是长痛,每次要做点什么事情都会遇到。第二点倒还好,短痛。第三点跟业务工程师没啥关系。
那么如果不使用派生,我们应该使用什么手段?
我的建议是使用AOP。
在架构师实现具体的方案之前,必须要想清楚几个问题,然后才能决定采用哪种方案。是哪几个问题?
- 方案的效果,和最终要达到的目的是什么?
- 在自己的知识体系里面,是否具备实现这个方案的能力?
- 在业界已有的开源组件里面,是否有可以直接拿来用的轮子?
这三个问题按照顺序一一解答之后,具体方案就能出来了。
我们先看第一个问题:方案的效果,和最终要达到的目的是什么?
方案的效果应该是:
- 业务方可以不用通过继承的方法,然后框架能够做到对ViewController的统一配置。
- 业务方即使脱离框架环境,不需要修改任何代码也能够跑完代码。业务方的ViewController一旦丢入框架环境,不需要修改任何代码,框架就能够起到它应该起的作用。
其实就是要实现不通过业务代码上对框架的主动迎合,使得业务能够被框架感知这样的功能。细化下来就是两个问题,框架要能够拦截到ViewController
的生命周期,另一个问题就是,拦截的定义时机。
对于方法拦截,很容易想到Method Swizzling
,那么我们可以写一个实例,在App启动的时候添加针对UIViewController
的方法拦截,这是一种做法。还有另一种做法就是,使用NSObject
的load
函数,在应用启动时自动监听。使用后者的好处在于,这个模块只要被项目包含,就能够发挥作用,不需要在项目里面添加任何代码。
然后另外一个要考虑的事情就是,原有的TMViewController
(所谓的父类)也是会提供额外方法方便子类使用的,Method Swizzling
只支持针对现有方法的操作,拓展方法的话,嗯,当然是用Category啦。
我本人不赞成Category
的过度使用,但鉴于Category
是最典型的化继承为组合的手段,在这个场景下还是适合使用的。还有的就是,关于Method Swizzling
手段实现方法拦截,业界也已经有了现成的开源库:Aspects,我们可以直接拿来使用。
我这边有个非常非常小的Demo可以放出来给大家,这个Demo只是一个点睛之笔,有一些话我也写在这个Demo里面了,各位架构师们你们可以基于各自公司App的需求去拓展。
这个Demo不包含Category
,毕竟Category
还是得你们自己去写啊~然后这套方案能够完成原来通过派生手段所有可以完成的任务,但同时又允许业务方不必添加任何代码,直接使用原生的UIViewController
。
然后另外要提醒的是,这方案的目的是消除不必要的继承,虽然不限定于UIViewController,但它也是有适用范围的,在适用继承的地方,还是要老老实实使用继承。比如你有一个数据模型,是由基本模型派生出的一整套模型,那么这个时候还是老老实实使用继承。至于拿捏何时使用继承,相信各位架构师一定能够处理好,或者你也可以参考我前面提到的那篇文章来控制拿捏的尺度。
关于MVC、MVVM等一大堆思想
其实这些都是相对通用的思想,万变不离其宗的还是在开篇里面我提到的那三个角色:数据管理者,数据加工者,数据展示者
。这些五花八门的思想,不外乎就是制订了一个规范,规定了这三个角色应当如何进行数据交换。但同时这些也是争议最多的话题,所以我在这里来把几个主流思想做一个梳理,当你在做View层架构时,能够有个比较好的参考。
MVC
MVC(Model-View-Controller)
是最老牌的的思想,老牌到4人帮的书里把它归成了一种模式,其中Model就是作为数据管理者
,View作为数据展示者
,Controller作为数据加工者
,Model和View又都是由Controller来根据业务需求调配,所以Controller还负担了一个数据流调配的功能。正在我写这篇文章的时候,我看到InfoQ发了这篇文章,里面提到了一个移动开发中的痛点是:对MVC架构划分的理解。我当时没能够去参加这个座谈会,也没办法发表个人意见,所以就只能在这里写写了。
在iOS开发领域,我们应当如何进行MVC的划分?
这里面其实有两个问题:
- 为什么我们会纠结于iOS开发领域中MVC的划分问题?
- 在iOS开发领域中,怎样才算是划分的正确姿势?
为什么我们会纠结于iOS开发领域中MVC的划分问题?
关于这个,每个人纠结的点可能不太一样,我也不知道当时座谈会上大家的观点。但请允许我猜一下:是不是因为UIViewController中自带了一个View,且控制了View的整个生命周期(viewDidLoad,viewWillAppear...),而在常识中我们都知道Controller不应该和View有如此紧密的联系,所以才导致大家对划分产生困惑?
,下面我会针对这个猜测来给出我的意见。
在服务端开发领域,Controller和View
的交互方式一般都是这样,比如Yii
:
/*
...
数据库取数据
...
处理数据
...
*/
// 此处$this就是Controller
$this->render("plan",array(
'planList' => $planList,
'plan_id' => $_GET['id'],
));
这里Controller
和View
之间区分得非常明显,Controller
做完自己的事情之后,就把所有关于View
的工作交给了页面渲染引擎去做,Controller
不会去做任何关于View的事情,包括生成View
,这些都由渲染引擎代劳了。这是一个区别,但其实服务端View
的概念和Native
应用View的概念,真正的区别在于:从概念上严格划分的话,服务端其实根本没有View,拜HTTP协议所赐,我们平时所讨论的View只是用于描述View的字符串(更实质的应该称之为数据),真正的View是浏览器
。
所以服务端只管生成对View的描述,至于对View的长相,UI事件监听和处理,都是浏览器负责生成和维护的。但是在Native这边来看,原本属于浏览器的任务也逃不掉要自己做。那么这件事情由谁来做最合适?苹果给出的答案是:UIViewController
。
鉴于苹果在这一层做了很多艰苦卓绝的努力,让iOS工程师们不必亲自去实现这些内容。而且,它把所有的功能都放在了UIView
上,并且把UIView
做成不光可以展示UI,还可以作为容器的一个对象。
看到这儿你明白了吗?UIView
的另一个身份其实是容器!UIViewController
中自带的那个view
,它的主要任务就是作为一个容器。如果它所有的相关命名都改成ViewContainer
,那么代码就会变成这样:
- (void)viewContainerDidLoad
{
[self.viewContainer addSubview:self.label];
[self.viewContainer addSubview:self.tableView];
[self.viewContainer addSubview:self.button];
[self.viewContainer addSubview:self.textField];
}
仅仅改了个名字,现在是不是感觉清晰了很多?如果再要说详细一点,我们平常所认为的服务端MVC是这样划分的:
---------------------------
| C |
| Controller |
| |
---------------------------
/ \
/ \
/ \
------------ ---------------------
| M | | V |
| Model | | Render Engine |
| | | + |
------------ | HTML Files |
---------------------
但事实上,整套流程的MVC划分是这样:
---------------------------
| C |
| Controller |
| \ |
| Render Engine |
| + |
| HTML Files |
---------------------------
/ \
/ \ HTML String
/ \
------------ ---------------
| M | | V |
| Model | | Browser |
| | | |
------------ ---------------
由图中可以看出,我们服务端开发在这个概念下,其实只涉及M和C的开发工作,浏览器作为View的容器,负责View的展示和事件的监听。那么对应到iOS客户端的MVC划分上面来,就是这样:
----------------------------
| C |
| Controller |
| \ |
| View Container |
----------------------------
/ \
/ \
/ \
------------ ----------------------
| M | | V |
| Model | | UITableView |
| | | YourCustomView |
------------ | ... |
----------------------
唯一区别在于,View的容器在服务端,是由Browser负责,在整个网站的流程中,这个容器放在Browser是非常合理的。在iOS客户端,View的容器是由UIViewController中的view负责,我也觉得苹果做的这个选择是非常正确明智的。
因为浏览器和服务端之间的关系非常松散,而且他们分属于两个不同阵营,服务端将对View
的描述生成之后,交给浏览器去负责展示,然而一旦view上有什么事件产生,基本上是很少传递到服务器(也就是所谓的Controller
)的(要传也可以:AJAX
),都是在浏览器这边把事情都做掉,所以在这种情况下,View容器就适合放在浏览器(V)这边。
但是在iOS开发领域,虽然也有让View
去监听事件的做法,但这种做法非常少,都是把事件回传给Controller
,然后Controller
再另行调度。所以这时候,View
的容器放在Controller
就非常合适。Controller
可以因为不同事件的产生去很方便地更改容器内容,比如加载失败时,把容器内容换成失败页面的View
,无网络时,把容器页面换成无网络的View
等等。
在iOS开发领域中,怎样才算是MVC划分的正确姿势?
这个问题其实在上面已经解答掉一部分了,那么这个问题的答案就当是对上面问题的一个总结吧。
M应该做的事:
- 给ViewController提供数据
- 给ViewController存储数据提供接口
- 提供经过抽象的业务基本组件,供Controller调度
C应该做的事:
- 管理
View Container
的生命周期 - 负责生成所有的View实例,并放入
View Container
- 监听来自
View
与业务有关的事件,通过与Model
的合作,来完成对应事件的业务。
V应该做的事:
- 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
- 界面元素表达
我通过与服务端MVC划分的对比来回答了这两个问题,之所以这么做,是因为我知道有很多iOS工程师之前是从服务端转过来的。我也是这样,在进安居客之前,我也是做服务端开发的,在学习iOS的过程中,我也曾经对iOS领域的MVC划分问题产生过疑惑,我疑惑的点就是前面开篇我猜测的点。如果有人问我iOS中应该怎么做MVC的划分,我就会像上面这么回答。
MVCS
苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分是Model
部分,拆出来一个Store
。这个Store
专门负责数据存取。但从实际操作的角度上讲,它拆开的是Controller
。
这算是瘦Model
的一种方案,瘦Model
只是专门用于表达数据,然后存储、数据处理都交给外面的来做。MVCS
使用的前提是,它假设了你是瘦Model
,同时数据的存储和处理都在Controller
去做。所以对应到MVCS
,它在一开始就是拆分的Controller
。因为Controller
做了数据存储的事情,就会变得非常庞大,那么就把Controller
专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store
。这么调整之后,整个结构也就变成了真正意义上的MVCS
。
关于胖Model和瘦Model
我在面试和跟别人聊天时,发现知道胖Model
和瘦Model
的概念的人不是很多。大约两三年前国外业界曾经对此有过非常激烈的讨论,主题就是Fat model, skinny controller
。现在关于这方面的讨论已经不多了,然而直到今天胖Model
和瘦Model
哪个更好,业界也还没有定论,所以这算是目前业界悬而未解的一个争议。我很少看到国内有讨论这个的资料,所以在这里我打算补充一下什么叫胖Model
什么叫瘦Model
。以及他们的争论来源于何处。
什么叫胖Model?
胖Model
包含了部分弱业务逻辑。胖Model
要达到的目的是,Controller
从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。举个例子:
Raw Data:
timestamp:1234567
FatModel:
@property (nonatomic, assign) CGFloat timestamp;
- (NSString *)ymdDateString; // 2015-04-20 15:16
- (NSString *)gapString; // 3分钟前、1小时前、一天前、2015-3-13 12:34
Controller:
self.dateLabel.text = [FatModel ymdDateString];
self.gapLabel.text = [FatModel gapString];
把timestamp
转换成具体业务上所需要的字符串,这属于业务代码,算是弱业务。FatModel
做了这些弱业务之后,Controller
就能变得非常skinny
,Controller
只需要关注强业务代码就行了。众所周知,强业务变动的可能性要比弱业务大得多,弱业务相对稳定,所以弱业务塞进Model
里面是没问题的。另一方面,弱业务重复出现的频率要大于强业务,对复用性的要求更高,如果这部分业务写在Controller
,类似的代码会洒得到处都是,一旦弱业务有修改(弱业务修改频率低不代表就没有修改),这个事情就是一个灾难。如果塞到Model
里面去,改一处很多地方就能跟着改,就能避免这场灾难。
然而其缺点就在于,胖Model
相对比较难移植,虽然只是包含弱业务,但好歹也是业务,迁移的时候很容易拔出萝卜带出泥。另外一点,MVC的架构思想更加倾向于Model
是一个Layer
,而不是一个Object
,不应该把一个Layer
应该做的事情交给一个Object
去做。最后一点,软件是会成长的,FatModel
很有可能随着软件的成长越来越Fat,最终难以维护。
什么叫瘦Model?
瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller
。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。举个例子:
Raw Data:
{
"name":"casa",
"sex":"male",
}
SlimModel:
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *sex;
Helper:
#define Male 1;
#define Female 0;
+ (BOOL)sexWithString:(NSString *)sex;
Controller:
if ([Helper sexWithString:SlimModel.sex] == Male) {
...
}
由于SlimModel
跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper
或其他的对象,来完成业务。在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel
只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel
也不会大到哪儿去。
缺点就在于,Helper这种做法也不见得很好,这里有一篇文章批判了这个事情。另外,由于Model的操作会出现在各种地方,SlimModel在一定程度上违背了DRY(Don't Repeat Yourself)
的思路,Controller
仍然不可避免在一定程度上出现代码膨胀。
我的态度?嗯,我会在本门心法这一节里面说。
说回来,MVCS
是基于瘦Model
的一种架构思路,把原本Model
要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store
,在一定程度上降低了Controller
的压力。
MVVM
MVVM
去年在业界讨论得非常多,无论国内还是国外都讨论得非常热烈,尤其是在ReactiveCocoa
这个库成熟之后,ViewModel
和View
的信号机制在iOS下终于有了一个相对优雅的实现。MVVM
本质上也是从MVC
中派生出来的思想,MVVM
着重想要解决的问题是尽可能地减少Controller
的任务。不管MVVM
也好,MVCS
也好,他们的共识都是Controller
会随着软件的成长,变很大很难维护很难测试。只不过两种架构思路的前提不同,MVCS
是认为Controller
做了一部分Model
的事情,要把它拆出来变成Store
,MVVM
是认为Controller
做了太多数据加工的事情,所以MVVM把数据加工的任务从Controller
中解放了出来,使得Controller
只需要专注于数据调配的工作,ViewModel
则去负责数据加工并通过通知机制让View
响应ViewModel
的改变。
MVVM
是基于胖Model
的架构思路建立的,然后在胖Model
中拆出两部分:Model
和ViewModel
。关于这个观点我要做一个额外解释:胖Model
做的事情是先为Controller
减负,然后由于Model
变胖,再在此基础上拆出ViewModel
,跟业界普遍认知的MVVM
本质上是为Controller
减负这个说法并不矛盾,因为胖Model
做的事情也是为Controller
减负。
另外,我前面说MVVM
把数据加工的任务从Controller
中解放出来,跟MVVM
拆分的是胖Model
也不矛盾。要做到解放Controller
,首先你得有个胖Model
,然后再把这个胖Model
拆成Model
和ViewModel
。
那么MVVM究竟应该如何实现?
这很有可能是大多数人纠结的问题,我打算凭我的个人经验试图在这里回答这个问题,欢迎大家在评论区交流。
在iOS领域大部分MVVM
架构都会使用ReactiveCocoa
,但是使用ReactiveCocoa
的iOS
应用就是基于MVVM
架构的吗?那当然不是,我觉得很多人都存在这个误区,我面试过的一些人提到了ReactiveCocoa
也提到了MVVM
,但他们对此的理解肤浅得让我忍俊不禁。嗯,在网络层架构我会举出不使用ReactiveCocoa
的例子,现在举我感觉有点儿早。
MVVM的关键是要有View Model!而不是ReactiveCocoa(勘误2)
ViewModel
做什么事情?就是把RawData
变成直接能被View
使用的对象的一种Model
。举个例子:
Raw Data:
{
(
(123, 456),
(234, 567),
(345, 678)
)
}
这里的RawData
我们假设是经纬度,数字我随便写的不要太在意。然后你有一个模块是地图模块,把经纬度数组全部都转变成MKAnnotation
或其派生类对于Controller
来说是弱业务,(记住,胖Model就是用来做弱业务的),因此我们用ViewModel
直接把它转变成MKAnnotation
的NSArray
,交给Controller
之后Controller
直接就可以用了。
嗯,这就是ViewModel
要做的事情,是不是觉得很简单,看不出优越性?
安居客Pad应用也有一个地图模块,在这里我设计了一个对象叫做reformer
(其实就是ViewModel
),专门用来干这个事情。那么这么做的优越性体现在哪儿呢?
安居客分三大业务:租房、二手房、新房。这三个业务对应移动开发团队有三个API开发团队,他们各自为政,这就造成了一个结果:三个API团队回馈给移动客户端的数据内容虽然一致,但是数据格式是不一致的,也就是相同value
对应的key
是不一致的。但展示地图的ViewController
不可能写三个,所以肯定少不了要有一个API数据兼容的逻辑,这个逻辑我就放在reformer
里面去做了,于是业务流程就变成了这样:
用户进入地图页发起地图API请求
|
|
|
-----------------------------------------
| | |
| | |
新房API 二手房API 租房API
| | |
| | |
-----------------------------------------
|
|
|
获得原始地图数据
|
|
|
[APIManager fetchDataWithReformer:reformer]
|
|
|
MKAnnotationList
|
|
|
Controller
这么一来,原本复杂的MKAnnotation
组装逻辑就从Controller
里面拆分了出来,Controller
可以直接拿着Reformer
返回的数据进行展示。APIManager就属于Model
,reformer就属于ViewModel
。具体关于reformer
的东西我会放在网络层架构来详细解释。Reformer
此时扮演的ViewModel
角色能够很好地给Controller
减负,同时,维护成本也大大降低,经过reformer
产出的永远都是MKAnnotation
,Controller
可以直接拿来使用。
然后另外一点,还有一个业务需求是取附近的房源,地图API请求是能够hold
住这个需求的,那么其他地方都不用变,在fetchDataWithReformer
的时候换一个reformer
就可以了,其他的事情都交给reformer
。
那么ReactiveCocoa应该扮演什么角色?
不用ReactiveCocoa
也能MVVM
,用ReactiveCocoa
能更好地体现MVVM
的精髓。前面我举到的例子只是数据从API到View
的方向,View
的操作也会产生"数据",只不过这里的"数据"更多的是体现在表达用户的操作上,比如输入了什么内容,那么数据就是text
、选择了哪个cell
,那么数据就是indexPath
。那么在数据从view
走向API或者Controller
的方向上,就是ReactiveCocoa
发挥的地方。
我们知道,ViewModel
本质上算是Model
层(因为是胖Model
里面分出来的一部分),所以View
并不适合直接持有ViewModel
,那么View一旦产生数据了怎么办?扔信号扔给ViewModel
,用谁扔?ReactiveCocoa
。
在MVVM
中使用ReactiveCocoa
的第一个目的就是如上所说,View
并不适合直接持有ViewModel
。第二个目的就在于,ViewModel
有可能并不是只服务于特定的一个View
,使用更加松散的绑定关系能够降低ViewModel
和View
之间的耦合度。
那么在MVVM中,Controller扮演什么角色?
大部分国内外资料阐述MVVM
的时候都是这样排布的:View <-> ViewModel <-> Model
,造成了MVVM
不需要Controller
的错觉,现在似乎发展成业界开始出现MVVM
是不需要Controller
的的声音了。其实MVVM
是一定需要Controller
的参与的,虽然MVVM
在一定程度上弱化了Controller
的存在感,并且给Controller
做了减负瘦身(这也是MVVM的主要目的)。但是,这并不代表MVVM
中不需要Controller
,MMVC
和MVVM
他们之间的关系应该是这样
[站外图片上传中...(image-4995a0-1555766670103)]
View <-> C <-> ViewModel <-> Model
,所以使用MVVM
之后,就不需要Controller
的说法是不正确的。严格来说MVVM
其实是MVCVM
。从图中可以得知,Controller
夹在View
和ViewModel
之间做的其中一个主要事情就是将View
和ViewModel
进行绑定。在逻辑上,Controller
知道应当展示哪个View
,Controller
也知道应当使用哪个ViewModel
,然而View
和ViewModel
它们之间是互相不知道的,所以Controller
就负责控制他们的绑定关系,所以叫Controller/控制器
就是这个原因。
前面扯了那么多,其实归根结底就是一句话:在MVC的基础上,把C拆出一个ViewModel
专门负责数据处理的事情,就是MVVM
。然后,为了让View
和ViewModel
之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa
,因为苹果本身并没有提供一个比较适合这种情况的绑定方法。iOS领域里KVO,Notification,block,delegate和target-action
都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa
提供的RACSignal
来的优雅,如果不用ReactiveCocoa
,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM
。
在实际iOS应用架构中,MVVM
应该出现在了大部分创业公司或者老牌公司新App的iOS应用架构图中,据我所知易宝支付旗下的某个iOS应用就整体采用了MVVM
架构,他们抽出了一个Action
层来装各种ViewModel
,也是属于相对合理的结构。
所以Controller
在MVVM
中,一方面负责View
和ViewModel
之间的绑定,另一方面也负责常规的UI逻辑处理。
VIPER
VIPER(View,Interactor,Presenter,Entity,Routing
)。VIPER我并没有实际使用过,我是在objc.io上第13期看到的。
但凡出现一个新架构或者我之前并不熟悉的新架构,有一点我能够非常肯定,这货一定又是把MVC的哪个部分给拆开了(坏笑,做这种判断的理论依据在第一篇文章里面我已经讲过了)。事实情况是VIPER确实拆了很多很多,除了View没拆,其它的都拆了。
我提到的这两篇文章关于VIPER都讲得很详细,一看就懂。但具体在使用VIPER的时候会有什么坑或者会有哪些争议我不是很清楚,硬要写这一节的话我只能靠YY,所以我想想还是算了。如果各位读者有谁在实际App中采用VIPER架构的或者对VIPER很有兴趣的,可以评论区里面提出来,我们交流一下。
转载: # iOS应用架构谈 view层的组织和调用方案