iOS架构师之路:控制器(View Controller)瘦身设计
前言
古老的MVC架构是容易被iOS开发者理解和接受的设计模式,但是由于iOS开发的项目功能越来越负责庞大,项目代码也随之不断壮大,MVC的模糊定义导致我们的业务开发工程师很容易把大量的代码写到视图控制器中,行业中对这种控制器有个专业词汇Massive ViewControler(臃肿的视图控制器)。代码臃肿导致可读性可维护性差,而且这种不清晰的设计还有许多的副作用,比如代码重用性差。作为架构师需要关注项目的代码质量。指导业务开发工程师写出高质量,高健壮性,高可用的代码也是很重要的工作。因此需要知道一些为控制器瘦身的技巧,并在项目中帮助业务开发工程师合理的运用它们。本文翻译一篇国外优秀文章:Lighter View Controllers。
示例代码下载地址:JackieHoo's GitHub
分离数据源(Data Source)等协议(Protocol)
瘦身控制器的有效方法之一就是将实现 UITableViewDataSource 协议相关的代码封装成一个类(比如本文中的 ArraryDataSource )。如果你多用几次这个设计,你就会创建复用性高的封装类。
举个例子,示例工程中的类 Photos控制器实现如下数据源方法:
# pragma mark Pragma
- (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath {
return photos[(NSUInteger)indexPath.row];
}
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
return photos.count;
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier
forIndexPath:indexPath];
Photo* photo = [self photoAtIndexPath:indexPath];
cell.label.text = photo.name;
return cell;
}
上面示例的数据源的实现都与 NSArray 有关,还有一个方法的实现与 Photo 有关(Photo 与 Cell 呈一一对应关系)。下面让我们来把与 NSArray 相关的代码从 控制器中抽离出来,并改用 block 来设置 cell 的视图。当然你也可以用代理来实现,取决于你的个人喜好。
@implementation ArrayDataSource
- (id)itemAtIndexPath:(NSIndexPath*)indexPath {
return items[(NSUInteger)indexPath.row];
}
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
return items.count;
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];
id item = [self itemAtIndexPath:indexPath];
configureCellBlock(cell,item);
return cell;
}
@end
现在我们可以控制器中的三个数据源代理方法可以干掉,并且把 控制器的 dataSource 设置为 ArrayDataSource 的实例。
void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) {
cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
cellIdentifier:PhotoCellIdentifier
configureCellBlock:configureCell];
self.tableView.dataSource = photosArrayDataSource;
通过上面的方法,你就可以把设置 Cell 视图的工作从 控制器中抽离出来。现在你不需要再关心indexPath如何与 NSArrary 中的元素如何关联,当你需要将数组中的元素在其它 UITableView 中展示时你可以重用以上代码。你也可以在 ArrayDataSource 中实现更多的方法,比如tableView:commitEditingStyle:forRowAtIndexPath:。
这样做还能带来额外的好处,我们还可以针对这部分实现编写单独的单元测试。不仅仅针对NSArray,我们可以使用这种分离思路处理其他数据容器(比如NSDictionary)。
该技巧同样适用于其他 Protocol ,比如 UICollectionViewDataSource 。通过该协议,你可以定义出各种各样的 UICollectionViewCell 。假如有一天,你需要在代码在使用到 UICollectionView 来替代当前的 UITableView,你只需要修改几行 控制器中的代码即可完成替换。你甚至能够让你的 DataSource 类同时实现 UICollectionViewDataSource 协议和 UITableViewDataSource 协议。
把业务逻辑移至 Model
下面是一段位于 控制器中的代码,作用是找出针对用户active priority的一个列表。
- (void)loadPriorities {
NSDate* now = [NSDate date];
NSString* formatString = @"startDate <= %@ AND endDate >= %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];
self.priorities = [priorities allObjects];
}
然而,假如你把代码实现移至 User 的 Category 中,控制器中的代码将会更简洁、更清晰。
将以上代码移到User+Extension.m中
- (NSArray*)currentPriorities {
NSDate* now = [NSDate date];
NSString* formatString = @"startDate <= %@ AND endDate >= %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}
ViewController.m 中的代码可以改成这个鬼样子,是不是明显要简洁许多,可读性强很多呢。
- (void)loadPriorities {
self.priorities = [self.user currentPriorities];
}
实际开发中,有些代码很难移至 model 对象中,但是很明显这些代码与 model 对象有关。针对这种情况,我们可以创建一个 store 类,并把相关代码迁移进去。
创建 Store 类
在这个示例项目工程中,我们有一段用于从本地文件加载数据并解析的代码:
- (void)readArchive {
NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSURL *archiveURL = [bundle URLForResource:@"photodata"
withExtension:@"bin"];
NSAssert(archiveURL != nil, @"Unable to find archive in bundle.");
NSData *data = [NSData dataWithContentsOfURL:archiveURL
options:
error:NULL];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
_users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"];
_photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"];
[unarchiver finishDecoding];
}
控制器不应该负责以上的工作,控制器只要负责数据调度就可以了,数据获取的工作我们完全可以交给 store 对象来负责。通过将这些代码从 控制器中抽离出来,我们可以更容易复用、测试这些方法、同时让控制器变得更轻巧( Store 对象一般负责数据的加载、缓存、持久化。Store 对象也经常被称作 Service Layer 对象,或者 Repository 对象)。
将 Web Service 逻辑移至 Model 层
这与上一个主题非常相似:别把 Web Service 相关的代码写在 控制器中,应该把这部分代码抽离出来。并通过方法的回调对数据进行处理。
不仅如此,你还可以把处理异常情况的工作也转交给 Store 对象负责。
把视图相关的代码移至 View 层
同样构建视图(尤其是复杂视图)的代码也不应该写在 View Controller (关我毛事啊,我只负责调度和通信啊)中。要么使用Interface Builder ,要么封装一个 Vew 的子类来完成这部分工作。假设现在需要实现自定义一个日期选择器。我们应该新建一个 DatePickerView 的子类来完成构建视图的工作,而不是把这部分工作放在 View Controller 中完成。同样的,这将是你的代码更简洁,复用性更强。
除了用 code 的形式来实现自定义视图,你也可以使用 Interface Builder 来完成构建自定义视图的工作。很多人都认为 Interface Builder 只能用于为 View Controller 构建视图,其实不然,你可以通过单独的 nib 文件来加载在 Interface Builder中构建的自定义视图。在示例工程当中,我们创建了一个包含了 Photo Cell 视图的 PhotoCell.xib 文件。

如图所示,我们在 view 中创建了属性(无需设置 File’s Owner 对象)并把它们与 Interface Builder 中的视图关联起来。这个方法同样适用于构建其它自定义视图。
通讯
我们在控制器中经常需要与其它控制器、Model 、 View 进行通讯。虽然这本来就是 控制器应该负责处理的事情,但我们依然可以用尽可能少的代码完成我们控制器的负责的工作。
现在已经有很多成熟的方案来建立 控制器与 View 的通讯(例如 KVO 和 fetched results controllers)。然而 控制器之间的通讯目前还没有类似的方案可以借鉴。
在实际开发中,我们经常需要把 遇到需要把控制器持有的一些状态信息,传递到 多个 控制器的需求。通常我们会将这些状态信息保存在一个对象中然后传递给其他的视图控制器。这部分的瘦身技巧比较复杂,我留在以后再专门讲解吧。
结论
我们已经展示了一些瘦身控制器的方法。作为架构师我们不可能完全照搬这些设计技巧,但我们需要清楚我们这么做的目的,我们只有一个目标:使得代码更易于维护,只要架构师在review代码时时刻关注这个目标,我们可以就可以扩展这些技巧,灵活运用到项目中。通过了解这些方法,我们能够更好的处理好复杂的视图控制器,并且让这些视图控制器的代码更整洁,更清晰。
欢迎关注我的微信公众号:丁丁的coding日记

iOS架构师之路:控制器(View Controller)瘦身设计的更多相关文章
- iOS架构师之路:慎用继承
最近在看大神Casa的文章<跳出面向对象思想(一) 继承>,脑洞大开.文章给我们展示了一个随着产品需求不断变化的例子,该例子中通过继承实现不同页面的搜索视图和搜索逻辑的代码复用,随着产品需 ...
- IOS架构师之路:我对IOS架构的点点认识(大纲)
1.今天我鼓起了勇气,想纪录自己对IOS架构学习成长的点点滴滴. 从事IOS开发也有几年的时间,从刚開始最主要的语言.界面.逻辑,再到后面复杂点的线程.数据处理.网络请求.动画,最后到最复杂的底层音视 ...
- android大牛高焕堂最新力作-android架构师之路
android大牛高焕堂 个人介绍: Android专家顾问,台湾Android论坛主席,现任亚太地区Android技术大会主席,台湾Android领域框架开发联盟总架构师.发表100多篇Androi ...
- 架构师之路-在Dubbo中开发REST风格的远程调用
架构师之路:从无到有搭建中小型互联网公司后台服务架构与运维架构 http://www.roncoo.com/course/view/ae1dbb70496349d3a8899b6c68f7d10b 概 ...
- 【转】java架构师之路:JAVA程序员必看的15本书的电子版下载地址
作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...
- paip.java 架构师之路以及java高级技术
paip.java 架构师之路以及java高级技术 1. Annotation 设计模式... 概念满天飞.ORM,IOC,AOP. Validator lambda4j memcache. 对 ...
- Java架构师之路:JAVA程序员必看的15本书
作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水 ...
- 高焕堂《android从程序员到架构师之路》 YY讲坛直面大师学习架构设计
<android从程序员到架构师之路>YY讲坛活动: sundy携手高焕堂老师全程YY答疑 与大师一起,分享android技术 时间:7月21日下午2:00 报名联系QQ:22243 ...
- 基于libevent, libuv和android Looper不断演进socket编程 - 走向架构师之路 - 博客频道 - CSDN.NET
基于libevent, libuv和android Looper不断演进socket编程 - 走向架构师之路 - 博客频道 - CSDN.NET 基于libevent, libuv和android L ...
随机推荐
- MFC编程入门之二十一(常用控件:编辑框Edit Control)
上一节讲了静态文本框,本节讲的是编辑框(Edit Control)同样是一种很常用的控件,我们可以在编辑框中输入并编辑文本.在前面加法计算器的例子中已经演示了编辑框的基本应用.下面具体讲解编辑框的使用 ...
- Redis教程(三) list类型
一.概述: redis的list类型其实就是一个每个子元素都是string类型的双向链表.所以[lr]push和[lr]pop命令的算法时间复杂度都是O(1) 另外list会记录链表的长度.所以ll ...
- spring-boot 之 使用Admin监控应用
https://yq.aliyun.com/articles/2322 ************************************* 摘要: Spring Boot提供的监控接口,例如: ...
- iOS —— 字典遍历排序
字典NSDictionary一般的遍历方法都是: NSArray* arr = [yourdictonary allKeys]; for(NSString* str in arr) { NSLog(& ...
- css样式之 direction
今天看到 direction:rtl.一时有点发傻. 其实就是文本读取顺序.默认是ltr从左向右读.rtl是从右向左读取.
- Mybatis + Mysql 插入数据时中文乱码问题
近日跟朋友一起建立一个项目,用的是spring+mybatis+mysql. 今天碰到一个mybatis向mysql中插入数据时,中文显示为'???'的问题,拿出来说下. 对于数据库操作中出现的中文乱 ...
- css样式大全
字体属性:(font) 大小 {font-size: x-large;}(特大) xx-small;(极小) 一般中文用不到,只要用数值就可以,单位:PX.PD 样式 {font-style: obl ...
- Remove Duplicates from Sorted Array II [LeetCode]
Follow up for "Remove Duplicates":What if duplicates are allowed at most twice? For exampl ...
- biweb后台添加上传下载功能
1.数据库对应表添加字段 file varchar(100) 2.对应的后台 模块名/admin/addinfo.php里添加如下代码: //文件上传 if ((($_FILES[&quo ...
- PHP 中:: -> self $this 操作符的区别
访问PHP类中的成员变量或方法时, 如果被引用的变量或者方法被声明成const(定义常量)或者static(声明静态),那么就必须使用操作符::, 反之如果被引用的变量或者方法没有被声明成 const ...