前言html
《iOS应用架构谈 开篇》出来以后,不少人来催我赶忙出第二篇。这一篇文章出得至关艰难,由于公司里的破事儿特别多,我本身又有点私事儿,以致于能用来写博客的时间不够充分。java
如今好啦,第二篇出来了。react
当咱们开始设计View层的架构时,每每是这个App尚未开始开发,或者这个App已经发过几个版本了,而后此时须要作很是完全的重构。ios
通常也就是这两种时机会去作View层架构,基于这个时机的特殊性,咱们在这时候必须清楚认识到:View层的架构一旦实现或定型,在App发版后可修改的余地就已经很是之小了。由于它跟业务关联最为紧密,因此哪怕稍微动一点点,它所引起的蝴蝶效应都不见得是业务方可以hold住的。这样的状况,就要求咱们在实现这个架构时,代码必须得改得勤快,不能偷懒。也必须抱着充分的自我怀疑态度,作决策时要拿捏好尺度。git
View层的架构很是之重要,在我看来,这部分架构是这系列文章涉及4个方面最重要的一部分,没有之一。为何这么说?程序员
View层架构是影响业务方迭代周期的因素之一github
产品经理产生需求的速度会很是快,尤为是公司此时仍处于创业初期,在规模稍大的公司里面,产品经理也喜欢挖大坑来在leader面前刷存在感,好比阿里。这就致使业务工程师任务很是繁重。正常状况下让产品经理砍需求是不太可能的,所以做为架构师,在架构里有一些可作可不作的事情,最好仍是能作就作掉,不要偷懒。这能够帮业务方减负,编写代码的时候也能更加关注业务。web
我跟一些朋友交流的时候,他们都会或多或少地抱怨本身的团队迭代速度不够快,或者说,迭代速度不合理地慢。我认为迭代速度不是想提就能提的,迭代速度的影响因素有不少,一期PRD里的任务量和任务复杂度都会影响迭代周期能达到什么样的程度。抛开这些外在的不谈,从内在可能致使迭代周期达不到合理的速度的缘由来看,其中有一个缘由颇有可能就是View层架构没有作好,让业务工程师完成一个不算复杂的需求时,须要处理太多额外的事情。固然,开会多,工程师水平烂也属于迭代速度提不上去的内部缘由,但这个不属于本文讨论范围。还有,加班不是优化迭代周期的正确方式,嗯。面试
通常来讲,一个不够好的View层架构,主要缘由有如下五种:数据库
代码混乱不规范
过多继承致使的复杂依赖关系
模块化程度不够高,组件粒度不够细
横向依赖
架构设计失去传承
这五个地方会影响业务工程师实现需求的效率,进而拖慢迭代周期。View架构的其余缺陷也会或多或少地产生影响,但在我看来这里五个是比较重要的影响因素。若是你们以为还有什么因素比这四个更高的,能够在评论区提出来我补上去。
对于第五点我想作一下强调:架构的设计是必定须要有传承的,有传承的架构从总体上看会很是协调。但实际状况有多是一我的走了,另外一个顶上,即使任务交接得再完整,都不可避免不一样的人有不一样的架构思路,从而致使整个架构的流畅程度受到影响。要解决这个问题,一方面要尽可能避免单点问题,让架构师作架构的时候再带一我的。另外一方面,架构要设计得尽可能简单,平缓接手人的学习曲线。我离开安居客的时候,作过保证:凡是从我手里出来的代码,终身保修。因此不要想着离职了就什么事儿都无论了,这不光是职业素养问题,还有一个是你对你的代码是否足够自信的问题。传承性对于View层架构很是重要,由于它距离业务最近,改动余地最小。
因此当各位CTO、技术总监、TeamLeader们以为迭代周期不够快时,你能够先不忙着急吼吼地去招新人,《人月神话》早就说过加人不能彻底解决问题。这时候若是你能够回过头来看一下是否是View层架构不合理,把这个弄好也是优化迭代周期的手段之一。
嗯,至于本系列其余三项的架构方案对于迭代周期的影响程度,我认为都不如View层架构方案对迭代周期的影响高,因此这是我认为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的代码应该差很少是这样:
要点以下:
全部的属性都使用getter和setter
不要在viewDidLoad里面初始化你的view而后再add,这样代码就很难看。在viewDidload里面只作addSubview的事情,而后在viewWillAppear里面作布局的事情(勘误1),最后在viewDidAppear里面作Notification的监听之类的事情。至于属性的初始化,则交给getter去作。
好比这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
#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去作了。总之就是尽可能不要出现如下的状况:
1
2
3
4
5
6
7
8
9
10
|
- (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,下至基层员工都有可能对产品设计评头论足,只要他我的有个地方用得不爽(极大多是我的喜爱)而后又正好跟产品经理比较熟悉可以搭得上话,都会提出各类意见。产品经理躲不起也惹不起,有时也是没办法,嗯。
但落实到工程师这边来,这种状况就很蛋疼。由于这种改变有时候不光是UI,UI所对应的逻辑也有要改的可能,工程师就会两边文件都改,你原来link的那个view如今不link了,而后你的outlet对应也要删掉,这两部分只要有一个没作,编译经过以后跑一下App,一下子就crash了。看起来这不是什么大事儿,但很影响心情。
另外,若是出现部分的代码复用,好比说某页面下某个View也但愿放在另一个页面里,相关的操做就不是复制粘贴这么简单了,你还得从新link一遍。也很影响心情。
复杂界面元素,复杂动画交互场景的开发任务比较多。
要是想在基于StoryBoard的项目中作一个动画,很烦。作几个复杂界面元素,也很烦。有的时候咱们挂Custom View上去,其实在StoryBoard里面看来就是一个空白View。而后另一点就是,当你的layout出现问题须要调整的时候,仍是挺难找到问题所在的,尤为是在复杂界面元素的状况下。
因此在针对View层这边的要求时,我也是建议不要用StoryBoard。实现简单的东西,用Code同样简单,实现复杂的东西,Code比StoryBoard更简单。因此我更加提倡用code去画view而不是storyboard。
是否有必要让业务方统一派生ViewController
有的时候咱们出于记录用户操做行为数据的须要,或者统一配置页面的目的,会从UIViewController里面派生一个本身的ViewController,来执行一些通用逻辑。好比天猫客户端要求全部的ViewController都要继承自TMViewController。这个统一的父类里面针对一个ViewController的全部生命周期都作了一些设置,至于这里都有哪些设置对于本篇文章来讲并不重要。在这里我想讨论的是,在设计View架构时,若是为了可以达到统一设置或执行统一逻辑的目的,使用派生的手段是有必要的吗?
我以为没有必要,为何没有必要?
使用派生比不使用派生更容易增长业务方的使用成本
不使用派生手段同样也能达到统一设置的目的
这两条缘由是我认为没有必要使用派生手段的理由,若是两条理由你都心照不宣,那么下面的就能够不用看了。若是你还有点疑惑,请看下面我来详细讲一下缘由。
为何使用了派生,业务方的使用成本会提高?
其实不光是业务方的使用成本,架构的维护成本也会上升。那么具体的成本都来自于哪里呢?
集成成本
这里讲的集成成本是这样的:若是业务方本身开了一个独立demo,快速完成了某个独立流程,如今他想把这个现有流程集合进去。那么问题就来了,他须要把全部独立的UIViewController改变成TMViewController。那为何不是一开始就马上使用TMViewController呢?由于要想引入TMViewController,就要引入整个天猫App全部的业务线,全部的基础库,由于这个父类里面涉及数日猫环境才有的内容,所谓拔出萝卜带出泥,你要是想简单继承一下就能搞定的事情,搭环境就要搞半天,而后这个小Demo才能跑得起来。
对于业务层存在的全部父类来讲,它们是很容易跟项目中的其余代码纠缠不清的,这使得业务方开发时遇到一个两难问题:要么把全部依赖所有搞定,而后基于App环境(好比天猫)下开发Demo,要么就是本身Demo写好以后,按照环境要求改代码。这里的两难问题都会带来成本,都会影响业务方的迭代进度。
我不肯定各位所在公司是否会有这样的状况,但我能够在这里给你们举一个我在阿里的真实的例子:我最近在开发某滤镜Demo和相关页面流程,最终是要合并到天猫这个App里面去的。使用天猫环境进行开发的话,pod install完全部依赖差很少须要10分钟,而后打开workspace以后,差很少要再等待1分钟让xcode作好索引,而后才能正式开始工做。在这里要感谢一下则平,由于他在此基础上作了不少优化,使得这个1分钟已经比原来的时间短不少了。但若是天猫环境有更新,你就要再重复一次上面的流程,不然 就颇有可能编译不过。
拜托,我只是想作个Demo而已,不想搞那么复杂。
上手接受成本
新来的业务工程师有的时候不见得都记得每个ViewController都必需要派生自TMViewController而不是直接的UIViewController。新来的工程师他不能直接按照苹果原生的作法去作事情,他须要额外学习,好比说:全部的ViewController都必须继承自TMViewController。
架构的维护难度
尽量少地使用继承能提升项目的可维护性,具体内容我在《跳出面向对象思想(一) 继承》里面说了,在这里我想偷懒不想把那篇文章里说过的东西再说一遍。
其实对于业务方来讲,主要仍是第一个集成成本比较蛋疼,由于这是长痛,每次要作点什么事情都会遇到。第二点倒还好,短痛。第三点跟业务工程师没啥关系。
那么若是不使用派生,咱们应该使用什么手段?
个人建议是使用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:
1
2
3
4
5
6
7
8
9
10
11
12
|
/*
...
数据库取数据
...
处理数据
...
*/
// 此处$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,那么代码就会变成这样:
1
2
3
4
5
6
7
8
|
- (void)viewContainerDidLoad
{
[self.viewContainer addSubview:self.label];
[self.viewContainer addSubview:self.tableView];
[self.viewContainer addSubview:self.button];
[self.viewContainer addSubview:self.textField];
}
... ...
|
仅仅改了个名字,如今是否是感受清晰了不少?若是再要说详细一点,咱们日常所认为的服务端MVC是这样划分的:
但事实上,整套流程的MVC划分是这样:
由图中能够看出,咱们服务端开发在这个概念下,其实只涉及M和C的开发工做,浏览器做为View的容器,负责View的展现和事件的监听。那么对应到iOS客户端的MVC划分上面来,就是这样:
惟一区别在于,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上。举个例子:
1
2
3
4
5
6
7
8
9
|
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。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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。举个例子:
1
2
3
4
5
6
7
8
|
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里面去作了,因而业务流程就变成了这样:
这么一来,本来复杂的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他们之间的关系应该是这样:
(来源:http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/)
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颇有兴趣的,能够评论区里面提出来,咱们交流一下。
本门心法
重剑无锋,大巧不工。 ---- 《神雕侠侣》
这是杨过在挑剑时,玄铁重剑旁边写的一段话。对此我深表认同。提到这段话的目的是想告诉你们,在具体作View层架构的设计时,不须要拘泥于MVC、MVVM、VIPER等规矩。这些都是招式,告诉你你就知道了,而后怎么玩均可以。可是心法不是这样的,心法是大巧,说出来很简单,可是能不能在实际架构设计时牢记心法,而且按照规矩办事,就都看我的了。
拆分的心法
天下功夫出少林,天下架构出MVC。 ---- Casa Taloyum
MVC实际上是很是高Level的抽象,意思也就是,在MVC体系下还能够再衍生无数的架构方式,但万变不离其宗的是,它必定符合MVC的规范。这句话不是我说的,是我在某个英文资料上看到的,但时过境迁,我已经找不到出处了,我很赞同这句话。我采用的架构严格来讲也是MVC,但也作了不少的拆分。根据前面几节的洗礼,相信各位也明白了这样的道理:拆分方式的不一样诞生了各类不一样的衍生架构方案(MVCS拆胖Controller,MVVM拆胖Model,VIPER什么都拆),但即使拆分方式再怎么多样,那都只是招式。而拆分的规范,就是心法。这一节我就讲讲我在作View架构时,作拆分的心法。
第一心法:保留最重要的任务,拆分其它不重要的任务
在iOS开发领域内,UIViewController承载了很是多的事情,好比View的初始化,业务逻辑,事件响应,数据加工等等,固然还有更多我如今也列举不出来,可是咱们知道有一件事情Controller确定逃不掉要作:协调V和M。也就是说,无论怎么拆,协调工做是拆不掉的。
那么剩下的事情咱们就能够拆了,好比UITableView的DataSource。唐巧的博客有一篇文章提到他和另外一个工程师关因而否要拆分DataSource争论了很久。拆分DataSource这个作法应该也算是通用作法,在不复杂的应用里面,它可能确实看上去只是一个数组而已,但在复杂的状况下,它背后可能涉及了文件内容读取,数据同步等等复杂逻辑,这篇文章的第一节就提倡了这个作法,我其实也蛮提倡的。
前面的文章里面也提了不少能拆的东西,我就不搬运了,你们能够进去看看。除了这篇文章提到的内容之外,任何比较大的,放在ViewController里面比较脏的,只要不是Controller的核心逻辑,均可以考虑拆出去,而后在架构的时候做为一个独立模块去定义,以及设计实现。
第二心法:拆分后的模块要尽量提升可复用性,尽可能作到DRY
根据第一心法拆开来的东西,颇有可能仍是强业务相关的,这种状况有的时候没法避免。但咱们拆也要拆得好看,拆出来的部分最好可以归成某一类对象,而后最好可以抽象出一个通用逻辑出来,使他可以复用。即便不能抽出通用逻辑,那也尽可能抽象出一个protocol,来实现IOP。这里有篇关于IOP的文章,你们看了就明白优越性了。
第三心法:要尽量提升拆分模块后的抽象度
也就是说,拆分的粒度要尽量大一点,封装得要透明一些。唐巧说一切隐藏都是对代码复杂性的增长,除非它带来了好处,这在必定程度上有点道理,没有好处的隐藏确实都很差(笑)。提升抽象度事实上就是增长封装的力度,将一个负责的业务抽象成只须要不多的输入就能完成,就是高度抽象。嗯,继承不少层,这种作法虽然也提升了抽象程度,但我不建议这么玩。我不肯定唐巧在这里说的隐藏跟我说的封装是否是同一个概念,但我在这里想提倡的是尽量提升抽象程度。
提升抽象程度的好处在于,对于业务方来讲,他只须要收集不多的信息(最小充要条件),作不多的调度(Controller负责大模块调度,大模块里面再去作小模块的调度),就可以完成任务,这才是给Controller减负的正确姿式。
若是拆分出来的模块抽象程度不够,模块对外界要求的参数比较多,那么在Controller里面,关于收集参数的代码就会多了不少。若是一部分参数的收集逻辑可以由模块来完成,那也能够作到帮Controller减轻负担。不然就感受拆得不太干净,由于Controller里面仍是多了一些没必要要的参数收集逻辑。
若是拆分出来的粒度过小,Controller在完成任务的时候调度代码要写不少,那也不太好。致使拆分粒度小的首要因素就是业务可能自己就比较复杂,拆分粒度小并非很差,能大就大一点,若是小了,那也没问题。针对这种状况的处理,就须要采用strategy模式。
针对拆分粒度小的状况,我来举个实际例子,这个例子来源于个人一个朋友他在作聊天应用的消息发送模块。当消息是文字时,直接发送。当消息是图片时,须要先向服务器申请上传资源,得到资源ID以后再上传图片,上传图片完成以后拿到图片URL,后面带着URL再把信息发送出去。
这时候咱们拆模块,能够拆成:数据发送(叫A模块),上传资源申请(叫B模块),内容上传(叫C模块)。那么要发送文字消息,Controller调度A就能够了。若是要发送图片消息,Controller调度B->C->A,假设未来还有上传别的类型消息的任务,他们又要依赖D/E/F模块,那这个事情就很蛋疼,由于逻辑复杂了,Controller要调度的东西要区分的状况就多了,Controller就膨胀了。
那么怎么处理呢?能够采用Strategy模式。咱们再来分析一下,Controller要完成任务,它初始状况下所具备的条件是什么?它有这条消息的全部数据,也知道这个消息的类型。那么它最终须要的是什么呢?消息发送的结果:发送成功或失败。
上面就是咱们要实现的最终结果,Controller只要把消息丢给MessageSender,而后让MessageSender去作事情,作完了告诉Controller就行了。那么MessageSender里面怎么去调度逻辑?MessageSender里面能够有一个StrategyList,里面存放了表达各类逻辑的Block或者Invocation(Target-Action)。那么咱们先定义一个Enum,里面规定了每种任务所须要的调度逻辑。
1
2
3
4
5
6
7
|
typedef NS_ENUM (NSUInteger, MessageSendStrategy)
{
MessageSendStrategyText = 0,
MessageSendStrategyImage = 1,
MessageSendStrategyVoice = 2,
MessageSendStrategyVideo = 3
}
|
而后在MessageSender里面的StrategyList是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@property (nonatomic, strong) NSArray *strategyList;
self.strategyList = @[TextSenderInvocation, ImageSenderInvocation, VoiceSenderInvocation, VideoSenderInvocation];
// 而后对外提供一个这样的接口,同时有一个delegate用来回调
- (void)sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;
@property (nonatomic, weak) id<messagesenderdelegate> delegate;
@protocol MessageSenderDelegate<nsobject>
@required
- (void)messageSender:(MessageSender *)messageSender
didSuccessSendMessage:(BaseMessage *)message
strategy:(MessageSendStrategy)strategy;
- (void)messageSender:(MessageSender *)messageSender
didFailSendMessage:(BaseMessage *)message
strategy:(MessageSendStrategy)strategy
error:(NSError *)error;
@end</nsobject></messagesenderdelegate>
|
Controller里面是这样使用的:
1
|
[self.messageSender sendMessage:message withStrategy:MessageSendStrategyText];
|
MessageSender里面是这样的:
1
|
[self.strategyList[strategy] invoke];
|
而后在某个Invocation里面,就是这样的:
1
2
3
|
[A invoke];
[B invoke];
[C invoke];
|
这样就好啦,即使拆分粒度由于客观缘由没法细化,那也能把复杂的判断逻辑和调度逻辑从Controller中抽出来,真正为Controller作到了减负。总之可以作到大粒度就尽可能大粒度,实在作不到那也行,用Strategy把它hold住。这个例子是小粒度的状况,大粒度的状况太简单,我就不举了。
设计心法
针对View层的架构不光是看重如何合理地拆分MVC来给UIViewController减负,另一点也要照顾到业务方的使用成本。最好的状况是业务方什么都不知道,而后他把代码放进去就能跑,同时还能得到框架提供的种种功能。
好比天安门广场上的观众看台,就是我以为最好的设计,由于没人会注意到它。
第一心法:尽量减小继承层级,涉及苹果原生对象的尽可能不要继承
继承是罪恶,尽可能不要继承。就我目前了解到的状况看,除了安居客的Pad App没有在框架级针对UIViewController有继承的设计之外,其它公司或多或少都针对UIViewController有继承,包括安居客iPhone app(那时候我已经对此无能为力,可见View的架构在一开始就设计好有多么重要)。甚至有的还对UITableView有继承,这是一件多么使人发指,多么惨绝人寰,多么丧心病狂的事情啊。虽然不可避免的是有些状况咱们不得不从苹果原生对象中继承,好比UITableViewCell。但我仍是建议尽可能不要经过继承的方案来给原生对象添加功能,前面提到的Aspect方案和Category方案均可以使用。用Aspect+load来实现重载函数,用Category来实现添加函数,固然,耍点手段用Category来添加property也是没问题的。这些方案已经覆盖了继承的所有功能,并且很是好维护,对于业务方也更加透明,何乐而不为呢。
不用继承可能在思路上不会那么直观,可是对于不使用继承带来的好处是足够顶得上使用继承的坏处的。顺便在此我要给Category正一下名:业界对于Category的态度比较暧昧,在多种场合(讲座、资料文档)都宣扬过尽量不要使用Category。它们说的都有必定道理,但我认为Category是苹果提供的最好的使用集合代替继承的方案,但针对Category的设计对架构师的要求也很高,请合理使用。并且苹果也在不少场合使用Category,来把一个本来可能很大的对象,根据不一样场景拆分红不一样的Category,从而提升可维护性。
不使用继承的好处我在这里已经说了,放到iOS应用架构来看,还能再多额外两个好处:1. 在业务方作业务开发或者作Demo时,能够脱离App环境,或花更少的时间搭建环境。2. 对业务方来讲功能更加透明,也符合业务方在开发时的第一直觉。
第二心法:作好代码规范,规定好代码在文件中的布局,尤为是ViewController
这主要是为了提升可维护性。在一个文件很是大的对象中,尤为要限制好不一样类型的代码在文件中的布局。好比在写ViewController时,我以前给团队制定的规范就是前面一段所有是getter setter,而后接下来一段是life cycle,viewDidLoad之类的方法都在这里。而后下面一段是各类要实现的Delegate,再下面一段就是event response,Button的或者GestureRecognizer的都在这里。而后后面是private method。通常状况下,若是作好拆分,ViewController的private method那一段是没有方法的。后来随着时间的推移,我发现开头放getter和setter太影响阅读了,因此后面改为全放在ViewController的最后。
第三心法:能不放在Controller作的事情就尽可能不要放在Controller里面去作
Controller会变得庞大的缘由,一方面是由于Controller承载了业务逻辑,MVC的总结者(在正式提出MVC以前,或多或少都有人这么设计,因此说MVC的设计者不太准确)对Controller下的定义也是承载业务逻辑,因此Controller就是用来干这事儿的,天经地义。另外一方面是由于在MVC中,关于Model和View的定义都很是明确,不多有人会把一个属于M或V的东西放到其余地方。而后除了Model和View之外,还会剩下不少模棱两可的东西,这些东西从概念上讲都算Controller,并且因为M和V定义得那么明确,因此直觉上看,这些东西放在M或V是不合适的,因而就往Controller里面塞咯。
正是因为上述两方面缘由致使了Controller的膨胀。咱们再细细思考一下,Model膨胀和View膨胀,要针对它们来作拆分其实都是相对容易的,Controller膨胀以后,拆分就显得艰难无比。因此若是可以在一开始就尽可能把能不放在Controller作的事情放到别的地方去作,这样在第一时间就可让你的那部分未来可能会被拆分的代码远离业务逻辑。因此咱们要稍微转变一下思路:模棱两可的模块,就不要塞到Controller去了,塞到V或者塞到M或者其余什么地方都比塞进Controller好,便于未来拆分。
因此关于前面我按下不表的关于胖Model和瘦Model的选择,个人态度是更倾向于胖Model。客观地说,业务膨胀以后,代码规模确定少不了的,无论你技术再好,经验再丰富,代码量最多只能优化,该膨胀仍是要膨胀的,并且优化以后代码每每也比较难看,使用各类奇技淫巧也是有代价的。因此,针对代码量优化的结果,每每要么就是牺牲可读性,要么就是牺牲可移植性(通用性),Every magic always needs a pay, you have to make a trade-off.。
那么既然膨胀出来的代码,或者未来有可能膨胀的代码,无论放在MVC中的哪个部分,最后都是要拆分的,既然早晚要拆分,那不如放Model里面,这样未来拆分胖Model也能比拆分胖Cotroller更加容易。在我还在安居客的时候,安居客Pad app承载最复杂业务的ViewController才不到600行,其余多数Controller都是在300-400行之间,这就为后面接手的人下降了很是多的上手难度和维护复杂度。拆分出来的东西都是能够直接迁移给iPhone app使用的。如今看天猫的ViewControler,动不动就几千行,看不了多久头就晕了,问了一下,你们都表示很习惯这样的代码长度,摊手。
第四心法:架构师是为业务工程师服务的,而不是去使唤业务工程师的
架构师在公司里的职级和地位每每都是要高于业务工程师的,架构师的技术实力和经验每每也都是高于业务工程师的。因此你值得在公司里得到较高的地位,可是在公司里的地位高不表明在软件工程里面的角色地位也高。架构师是要为业务工程师服务的,是他们使唤你而不是你使唤他们。另外,制定规范一方面是起到约束业务工程师的代码,但更重要的一点是,这实际上是利用你的能力帮助业务工程师避免他没法预见的危机,因此地位高有必定的好处,毕竟夏虫不可语冰,有的时候不见得可以解释得通,所以高地位随之而来的就是说服力会比较强。但在软件工程里,必定要保持谦卑,必定要多为业务工程师考虑。
一个不懂这个道理的架构师,设计出来的东西每每复杂难用,由于他只愿意作核心的东西,周边不肯意作的都指望交给业务工程师去作,甚至有的时候就只作了个Demo,而后就交给业务工程师了,业务工程师变成给他打工的了。可是一个懂得这个道理的架构师,设计出来的东西会很是好用,业务方只须要扔不多的参数而后拿结果就行了,这样的架构才叫好的架构。
举一个保存图片到本地的例子,一种作法是提供这样的接口:- (NSString *)saveImageWithData:(NSData *)imageData,另外一种是- (NSString *)saveImage:(UIImage *)image。后者更好,缘由本身想。
你的态度越谦卑,就越能设计出好的架构,这是我设计心法里的最后一条,也是最重要的一条。即便你如今技术实力不是业界大牛级别的,但只要保持这个心态去作架构,去作设计,就已是合格的架构师了,要成为业界大牛也会很是快。
小总结
其实针对View层的架构设计,仍是要作好三点:代码规范,架构模式,工具集。
代码规范对于View层来讲意义重大,毕竟View层很是重业务,若是代码布局混乱,后来者很难接手,也很难维护。
架构模式具体如何选择,彻底取决于业务复杂度。若是业务至关至关复杂,那就能够使用VIPER,若是相对简单,那就直接MVC稍微改改就行了。每一种已经成为定式的架构模式不见得都适合各自公司对应的业务,因此须要各位架构师根据状况去作一些拆分或者改变。拆分通常都不会出现问题,改变的时候,只要别把MVC三个角色搞混就行了,M该作啥作啥,C该作啥作啥,V该作啥作啥,不要乱来。关于大部分的架构模式应该是什么样子,这篇文章里都已经说过了,不过我认为最重要的仍是后面的心法,模式只是招术,熟悉了心法才能大巧不工。
View层的工具集主要仍是集中在如何对View进行布局,以及一些特定的View,好比带搜索提示的搜索框这种。这篇文章只提到了View布局的工具集,其它的工具集相对而言是更加取决于各自公司的业务的,各自实现或者使用CocoaPods里现成的都不是很难。
对于小规模或者中等规模iOS开发团队来讲,作好以上三点就足够了。在大规模团队中,有一个额外问题要考虑,就是跨业务页面调用方案的设计。
跨业务页面调用方案的设计
跨业务页面调用是指,当一个App中存在A业务,B业务等多个业务时,B业务有可能会须要展现A业务的某个页面,A业务也有可能会调用其余业务的某个页面。在小规模的App中,咱们直接import其余业务的某个ViewController而后或者push或者present,是不会产生特别大的问题的。可是若是App的规模很是大,涉及业务数量很是多,再这么直接import就会出现问题。
能够看出,跨业务的页面调用在多业务组成的App中会致使横向依赖。那么像这样的横向依赖,若是不去设法解决,会致使什么样的结果?
当一个需求须要多业务合做开发时,若是直接依赖,会致使某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,而整个需求完成的速度会变慢,影响的是团队开发迭代速度。
当要开辟一个新业务时,若是已有各业务间直接依赖,新业务又依赖某个旧业务,就致使新业务的开发环境搭建困难,由于必需要把全部相关业务都塞入开发环境,新业务才能进行开发。影响的是新业务的响应速度。
当某一个被其余业务依赖的页面有所修改时,好比更名,涉及到的修改面就会特别大。影响的是形成任务量和维护成本都上升的结果。
固然,若是App规模特别小,这三点带来的影响也会特别小,可是在阿里这样大规模的团队中,像天猫/淘宝这样大规模的App,一旦赶上这里面哪怕其中一件事情,就特么很坑爹。
那么应该怎样处理这个问题?
让依赖关系下沉。
怎么让依赖关系下沉?引入Mediator模式。
所谓引入Mediator模式来让依赖关系下沉,实质上就是每次呼唤页面的时候,经过一个中间人来召唤另一个页面,这样只要每一个业务依赖这个中间人就能够了,中间人的角色就能够放在业务层的下面一层,这就是依赖关系下沉。
当A业务须要调用B业务的某个页面的时候,将请求交给Mediater,而后由Mediater经过某种手段获取到B业务页面的实例,交还给A就好了。在具体实现这个机制的过程当中,有如下几个问题须要解决:
设计一套通用的请求机制,请求机制须要跟业务剥离,使得不一样业务的页面请求都可以被Mediater处理
设计Mediater根据请求如何获取其余业务的机制,Mediater须要知道如何处理请求,上哪儿去找到须要的页面
这个看起来就很是像咱们web开发时候的URL机制,发送一个Get或Post请求,CGI调用脚本把请求分发给某个Controller下的某个Action,而后返回HTML字符串到浏览器去解析。苹果自己也实现了一套跨App调用机制,它也是基于URL机制来运转的,只不过它想要解决的问题是跨App的数据交流和页面调用,咱们想要解决的问题是下降各业务的耦合度。
不过咱们还不能直接使用苹果原生的这套机制,由于这套机制不可以返回对象实例。而咱们但愿可以拿到对象实例,这样不光能够作跨业务页面调用,也能够作跨业务的功能调用。另外,咱们又但愿咱们的Mediater也可以跟苹果原生的跨App调用兼容,这样就又能帮业务方省掉一部分开发量。
就我目前所知道的状况,AutoCad旗下某款iOS应用(时间有点久我不记得是哪款应用了,若是你是AutoCad的iOS开发,能够在评论区补充一下。)就采用了这种页面调用方式。天猫里面目前也在使用这套机制,只是这一块因为历史缘由存在新老版本混用的状况,所以暂时还没可以很好地发挥应有的做用。
嗯,想问我要Demo的同窗,我能够很大方地告诉你,没有。不过我打算抽时间写一个出来,如今除了已经想好名字叫Summon之外,其它什么都没作,哈哈。
关于Getter和Setter?
我比较习惯一个对象的"私有"属性写在extension里面,而后这些属性的初始化所有放在getter里面作,在init和dealloc以外,是不会出现任何相似_property这样的写法的。就是这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@interface CustomObject()
@property (nonatomic, strong) UILabel *label;
@end
@implement
#pragma mark - life cycle
- (void)viewDidLoad
{
[
super
viewDidLoad];
[self.view addSubview:self.label];
}
- (void)viewWillAppear:(BOOL)animated
{
[
super
viewWillAppear:animated];
self.label.frame = CGRectMake(1, 2, 3, 4);
}
#pragma mark - getters and setters
- (UILabel *)label
{
if
(_label == nil) {
_label = [[UILabel alloc] init];
_label.text = @
"1234"
;
_label.font = [UIFont systemFontOfSize:12];
... ...
}
return
_label;
}
@end
|
唐巧说他喜欢的作法是用_property这种,而后关于_property的初始化经过[self setupProperty]这种作法去作。从刚才上面的代码来看,就是要在viewDidLoad里面多调用一个setup方法而已,而后我推荐的方法就是不用多调一个setup方法,直接走getter。
嗯,怎么说呢,其实两种作法都能完成需求。可是从另外一个角度看,苹果之因此选择让[self getProperty]和self.property能够互相通用,这种作法已经很明显地表达了苹果的倾向:但愿每一个property都是经过getter方法来得到。
早在2003年,Allen Holub就发了篇文章《Why getter and setter methods are evil》,自此以后,业界就对此产生了各类争议,虽然是从Java开始说的,可是发展到后面各类语言也参与了进来。而后虽然如今关于这个问题讨论得少了,可是依旧属于没有定论的状态。setter的状况比较复杂,也不是我这一节的重点,我这边仍是主要说getter。咱们从objc的设计来看,苹果的设计者更加倾向于getter is not evil。
认为getter is evil的缘由有很是之多,或大或小,随着争论的进行,你们慢慢就聚焦到这样的一个缘由:Getter和Setter提供了一个能让外部修改对象内部数据的方式,这是evil的,正常状况下,一个对象本身私有的变量应该是只有本身关心。
而后咱们回到iOS领域来,objc也一样面临了这样的问题,甚至更加严重:objc并无像Java那么严格的私有概念。但在实际工做中,咱们不太会去操做头文件里面没有的变量,这是从规范上就被禁止的。
认为getter is not evil的缘由也能够聚焦到一个:高度的封装性。getter事实上是工厂方法,有了getter以后,业务逻辑能够更加专一于调用,而没必要担忧当前变量是否可用。咱们能够想一下,假设一个ViewController有20个subview要加入view中,这20个subview的初始化代码是确定逃不掉的,放在哪里比较好?放在哪里都比放在addsubview的地方好,我我的认为最好的地方仍是放在getter里面,结合单例模式以后,代码会很是整齐,生产的地方和使用的地方获得了很好的区分。
因此放到iOS来讲,我仍是以为使用getter会比较好,由于evil的地方在iOS这边基本都避免了,not evil的地方都能享受到,仍是不错的。
总结
要作一个View层架构,主要就是从如下三方面入手:
制定良好的规范
选择好合适的模式(MVC、MVCS、MVVM、VIPER)
根据业务状况针对ViewController作好拆分,提供一些小工具方便开发
固然,你还会遇到其余的不少问题,这时候你能够参考这篇文章里提出的心法,在后面提到的跨业务页面调用方案的设计中,你也可以看到个人一些心法的影子。
对于iOS客户端来讲,它并不像其余语言诸如Python、PHP他们有那么多的非官方通用框架。客观缘由在于,苹果已经为咱们作了很是多的事情,作了不少的努力。在苹果已经作了这么多事情的基础上,架构师要作针对View层的方案时,最好仍是尽可能遵照苹果已有的规范和设计思想,而后根据本身过去开发iOS时的经验,尽量给业务方在开发业务时减负,提升业务代码的可维护性,就是View层架构方案的最大目标。
2015-04-28 09:28补:关于AOP
AOP(Aspect Oriented Programming),面向切片编程,这也是面向XX编程系列术语之一哈,但它跟咱们熟知的面向对象编程没什么关系。
什么是切片?
程序要完成一件事情,必定会有一些步骤,1,2,3,4这样。这里分解出来的每个步骤咱们能够认为是一个切片。
什么是面向切片编程?
你针对每个切片的间隙,塞一些代码进去,在程序正常进行1,2,3,4步的间隙能够跑到你塞进去的代码,那么你写这些代码就是面向切片编程。
为何会出现面向切片编程?
你要想作到在每个步骤中间作你本身的事情,不用AOP也同样能够达到目的,直接往步骤之间塞代码就行了。可是事实状况每每很复杂,直接把代码塞进去,主要问题就在于:塞进去的代码颇有多是跟原业务无关的代码,在同一份代码文件里面掺杂多种业务,这会带来业务间耦合。为了下降这种耦合度,咱们引入了AOP。
如何实现AOP?
AOP通常都是须要有一个拦截器,而后在每个切片运行以前和运行以后(或者任何你但愿的地方),经过调用拦截器的方法来把这个jointpoint扔到外面,在外面得到这个jointpoint的时候,执行相应的代码。
在iOS开发领域,objective-C的runtime有提供了一系列的方法,可以让咱们拦截到某个方法的调用,来实现拦截器的功能,这种手段咱们称为Method Swizzling。Aspects经过这个手段实现了针对某个类和某个实例中方法的拦截。
另外,也能够使用protocol的方式来实现拦截器的功能,具体实现方案就是这样:
1
2
3
4
5
6
7
8
9
10
11
12
|
@protocol RTAPIManagerInterceptor <nsobject>
@optional
- (void)manager:(RTAPIBaseManager *)manager beforePerformSuccessWithResponse:(AIFURLResponse *)response;
- (void)manager:(RTAPIBaseManager *)manager afterPerformSuccessWithResponse:(AIFURLResponse *)response;
- (void)manager:(RTAPIBaseManager *)manager beforePerformFailWithResponse:(AIFURLResponse *)response;
- (void)manager:(RTAPIBaseManager *)manager afterPerformFailWithResponse:(AIFURLResponse *)response;
- (BOOL)manager:(RTAPIBaseManager *)manager shouldCallAPIWithParams:(NSDictionary *)params;
- (void)manager:(RTAPIBaseManager *)manager afterCallingAPIWithParams:(NSDictionary *)params;
@end
@interface RTAPIBaseManager : NSObject
@property (nonatomic, weak) id<rtapimanagerinterceptor> interceptor;
@end</rtapimanagerinterceptor></nsobject>
|
这么作对比Method Swizzling有个额外好处就是,你能够经过拦截器来给拦截器的实现者提供更多的信息,便于外部实现更加了解当前切片的状况。另外,你还能够更精细地对切片进行划分。Method Swizzling的切片粒度是函数粒度的,本身实现的拦截器的切片粒度能够比函数更小,更加精细。
缺点就是,你得本身在每个插入点把调用拦截器方法的代码写上(笑),经过Aspects(本质上就是Mehtod Swizzling)来实现的AOP,就能轻松一些。
2015-4-29 14:25 补:关于在哪儿写Constraints?
文章发出来以后,不少人针对勘误1有不少见解,以致于我以为颇有必要在这里作一份补。期间过程不少很复杂,这篇文章也已经很长了,我就直接说结果了哈。
苹果在文档中指出,updateViewConstraints是用来作add constraints的地方。
可是在这里有一个回答者说updateViewConstraints并不适合作添加Constraints的事情。
综合我本身和评论区各位关心这个问题的兄弟们的各类测试和各类文档,我如今以为仍是在viewDidLoad里面开一个layoutPageSubviews的方法,而后在这个里面建立Constraints并添加,会比较好。就是像下面这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
- (void)viewDidLoad
{
[
super
viewDidLoad];
[self.view addSubview:self.firstView];
[self.view addSubview:self.secondView];
[self.view addSubview:self.thirdView];
[self layoutPageSubviews];
}
- (void)layoutPageSubviews
{
[self.view addConstraints:xxxConstraints];
[self.view addConstraints:yyyConstraints];
[self.view addConstraints:zzzConstraints];
}
|
最后,要感谢评论区各位关心这个问题,并提出本身意见,甚至是本身亲自测试而后告诉我结果的各位兄弟:@fly2never,@Wythe,@wtlucky,@lcddhr,@李新星,@Meigan Fang,@匿名,@Xiao Moch。
这个作法是目前我本身以为可能比较合适的作法,固然也欢迎其余同窗继续拿出本身的见解,咱们来讨论。
勘误
个人前同事@ddaajing看了这篇文章以后,给我提出了如下两个勘误,和不少行文上的问题。在这里我对他表示很是感谢:
勘误1:其实在viewWillAppear这里改变UI元素不是很可靠,Autolayout发生在viewWillAppear以后,严格来讲这里一般不作视图位置的修改,而用来更新Form数据。改变位置能够放在viewWilllayoutSubview或者didLayoutSubview里,并且在viewDidLayoutSubview肯定UI位置关系以后设置autoLayout比较稳妥。另外,viewWillAppear在每次页面即将显示都会调用,viewWillLayoutSubviews虽然在lifeCycle里调用顺序在viewWillAppear以后,可是只有在页面元素须要调整时才会调用,避免了Constraints的重复添加。
勘误2:MVVM要有ViewModel,以及ReactiveCocoa带来的信号通知效果,在ReactiveCocoa里就是RAC等相关宏来实现。另外,使用ReactiveCocoa可以比较优雅地实现MVVM模式,就是由于有RAC等相关宏的存在。就像它的名字同样Reactive-响应式,这也是区分MVVM的VM和MVC的C和MVP的P的一个重要方面。
本文遵照CC-BY。 请保持转载后文章内容的完整,以及文章出处。本人保留全部版权相关权利。