objc.io 学习笔记-ViewController瘦身记

####前言

objc.io 就不多介绍了,国外iOS高质量博客网站。我这里说一下我为什么要写这个系列。首先我觉的这种技术类博客,能看懂不代表你已经完全理解,也不代表你能灵活运用到自己的项目中。有时候我们看完了文章感觉受益匪浅,然后就该干嘛干嘛去了,随着时间的推移(时间是一个很可怕的东西),一些很赞的思路或者技术点就慢慢变的模糊不清。我不清楚其他人会不会有这样的问题,我反正是经常遇到这种问题,所以我觉的自己很有必要把原博客的内容进行一下归纳整理,方便日后查阅。今天主要记录一下 更轻量的 View Controllers 里边的技术点。

该篇主要针对的是ViewController,因为ViewController是iOS开发中用的最多的组件。

####将DataSource从ViewController中进行抽离

其实就是把UITableViewDataSource协议用一个单独的类来实现。抽离的目的是什么呢?当然是复用了,我们就不用在每个含有TableView的ViewController里边都实现一遍UITableViewDataSource协议了,省时省力,极大提高代码质量和编程效率。那该怎么抽离呢?

首先为实现DataSource协议的类起个名字吧,比如叫:ArrayDataSource。接下来看一下都要实现哪些协议:

1
2
3
4
5
6

- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section;

- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath;

第一个协议返回数组元素个数。
第二个协议返回一个UITableViewCell。

数组简单,直接传进去就好了,cell呢?cell的创建方法为:

1
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];//这种创建方式要求identifier已经注册到具体的Cell类型

其中 tableView和indexPath这两个参数都是通过协议方法返回的,直接用。所以只剩下Identifier这个参数需要我们自己设置。这样我们就确定了两个参数需要传到ArrayDataSource,一个是数组,一个是Identifier。这样是不是就没问题了呢?当然不是,因为每个TableView的Cell都是不一样的,同时每个数组元素的类型也是不确定的。怎么保证Cell的多态性呢?这是问题的关键

多态的用法一般就是进行类型转换,比如我们经常在代码里这么写

1
CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];

其实这个可以拆分成两行代码

1
2
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
CustomCell *customCell = cell;//类型转换

所以关键就是怎么做来实现类型转换,其实cell创建之后类型已经确定了,但是我们需要对其进行显式的转换以便对cell进行设置,两个思路,一个是delegate,一个是block。

delegate的话,可以定义一个类似这样的协议:

1
- (void)configCell:(id)cell withItem:(id)item;

然后在具体的ViewController实现如下:

1
2
3
4
5
- (void)configCell:(id)cell withItem:(id)item{
CustomCell *customCell = cell;
CustomModel *customModel = item;
[customCell setContent:customModel];//setContent方法需要在CustomCell实现
}

相对于delegate,我还是倾向于block,因为block结构紧凑,易于阅读,那么block该如何写呢?block的功能类似函数指针的功能。我们在具体的ViewController里定义好具体的参数类型以及实现,然后将block传到ArrayDataSource。那么ArrayDataSource的block类型应该怎么写呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef void (^TableViewCellConfigureBlock)(id cell, id item);//因为要复用,所以要写成id类型

- (id)initWithItems:(NSArray *)anItems
cellIdentifier:(NSString *)aCellIdentifier
configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock
{
self = [super init];
if (self) {
self.items = anItems;
self.cellIdentifier = aCellIdentifier;
self.configureCellBlock = [aConfigureCellBlock copy];
}
return self;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
forIndexPath:indexPath];
id item = [self itemAtIndexPath:indexPath];
self.configureCellBlock(cell,item);
return cell;
}

而具体的ViewController里应该这么写

1
2
3
4
5
6
7
8
void (^configureCell)(CustomCell*, CustomModel*) = ^(CustomCell* cell, CustomModel* model) {
[cell setContent:model];
};

photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
cellIdentifier:PhotoCellIdentifier
configureCellBlock:configureCell];
self.tableView.dataSource = photosArrayDataSource;

这样就搞定了,是不是很爽呢,ArrayDataSource就可以作为一个数据源类来使用了,省去好多重复代码。

同时还有一个小细节:

1
2
3
4
- (id)itemAtIndexPath:(NSIndexPath *)indexPath
{
return self.items[(NSUInteger) indexPath.row];
}

也是为了复用,对外只暴露indexPath参数,同时返回id类型。

####让 Cells 可复用

有时多种 model 对象需要用同一类型的 cell 来表示,这种情况下,我们怎么让 cell 复用呢?因为 [cell setContent:model];content的参数类型是确定的,那怎么做可以保证同一个cell可以设置不同的model呢?对,协议,只要遵循了该Cell协议的对象都可以作为-setContent:的参数。

首先,我们给 cell 定义一个 protocol,需要用这个 cell 对于的model对象必须遵循这个 protocol。类似这样:

1
2
3
4
5
@protocol CellConfigDelegate
- (NSString *)imageName;
- (NSString *)mainTitle;
- (NSString *)subTitle;
@end

然后简单修改 category 中的设置方法,让它可以接受遵循这个 protocol 的对象。

1
- (void)setContent:(id<CellConfigDelegate>)model;

这些简单的步骤让 cell 和任何特殊的 model 对象之间得以解耦,让它可适应不同的数据类型。

####在 Cell 内部控制 Cell 的状态
如果你想自定义 table views 默认的高亮或选择行为,你可以实现两个 delegate 方法,把点击的 cell 修改成我们想要的样子。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)tableView:(UITableView *)tableView
didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}

- (void)tableView:(UITableView *)tableView
didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = nil;
}

然而,这两个 delegate 方法的实现又基于了 view controller 知晓 cell 实现的具体细节。如果我们想替换或重新设计 cell,我们必须改写 delegate 代码。View 的实现细节和 delegate 的实现交织在一起了。我们应该把这些细节移到 cell 自身中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation PhotoCell
// ...
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
[super setHighlighted:highlighted animated:animated];
if (highlighted) {
self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
} else {
self.photoTitleLabel.shadowColor = nil;
}
}
@end

这样我们通过- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated中的bool参数就可以设置不同状态了。

总的来说,我们在努力把 view 层和 controller 层的实现细节分离开。delegate 肯定得清楚一个 view 该显示什么状态,但是它不应该了解如何修改 view 结构或者给某些 subviews 设置某些属性以获得正确的状态。所有这些逻辑都应该封装到 view 内部,然后给外部提供一个简单地 API。

####将业务逻辑移到Model层
比如一些格式化字符串,解析数据等都放到Model层去处理

####将网络请求移到Model层
对网络请求进行封装到单独的模块

####把 View相关代码移到 View 层
对于复杂的视图,我们要进行封装

####使用Child View Controllers
(这个单独来进行总结)

####搭建 Model 对象和 Cells 之间的桥梁
[cell setContent:model];

####总结
Table view controllers(以及其他的 controller 对象!)应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。你应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单地样板代码。

这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单地 API,最终,它变得更加容易理解,也更利于团队协作。