分离UITableView数据源和协议

View controllers 通常是 iOS 项目中最大的文件,并且它们包含了许多不必要的代码。所以 View controllers 中的代码几乎总是复用率最低的。接下来我将结合一些我自己看到的东西和平时在使用的方法,来节省ViewController中的代码量。

把 Data Source 和其他 Protocols 分离出来

我们在平时的编码中,最经常使用到的一个控件就是UITableView了,那我们每次需要使用到tableview的时候,都需要写一些重复的代码,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma mark - tableview datasource

#pragma mark - tableview datasource

- (PPSFriend *)friendAtIndexPath:(NSIndexPath *)indexPath{
return self.friends[indexPath.row];
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.friends.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
PPSFriendCell *cell = [tableView dequeueReusableCellWithIdentifier:@"friendCell" forIndexPath:indexPath];
PPSFriend *friend = [self friendAtIndexPath:indexPath];
cell.textLabel.text = friend.name;
return cell;
}

像上面一些代码,我们每天都在写,每次用到UITableView都需要重写这些毫无技术可言的代码,那我们试想一下能否写一个封装类,将这些重复的方法全部封起来,多次使用呢?答案当然是,可以的。

上面的代码,其实都是在围绕着friends这个数组做一系列的事情,我们可以独立出来一个类,使用一个block或者delegate来设置cell,当然这取决于你的习惯。

1
2
3
4
5
6
7
8
#pragma mark - tableview config cell
- (void)configCell{
void (^configCell)(PPSFriendCell *, PPSFriend *) = ^(PPSFriendCell *cell, PPSFriend *friend){
cell.textLabel.text = friend.name;
};
PPSArrayDatasource *datasources = [[PPSArrayDatasource alloc] initWithItems:self.friends cellIdentifier:@"friendCell" configureCellBlock:configCell];
self.tableView.dataSource = datasources;
}

现在,你可以把 view controller 中的这 3 个方法去掉了,取而代之,你可以创建一个 PPSArrayDatasource 类的实例作为 table view 的 data source。

现在你不用担心把一个 index path 映射到数组中的位置了,每次你想把这个数组显示到一个 table view 中时,你都可以复用这些代码。你也可以实现一些额外的方法,比如

1
tableView:commitEditingStyle:forRowAtIndexPath:

多个section

还有一种情况,如果是多个section的情况下,我们还可以再扩展一下,将block定义为

1
typedef void(^TableViewCellConfigureBlock)(id cell, id item, NSIndexPath *indexPath);
1
2
3
4
5
6
7
8
#pragma mark - tableview config cell
- (void)configCell{
void (^configCell)(PPSFriendCell *, PPSFriend *, NSIndexPath *) = ^(PPSFriendCell *cell, PPSFriend *friend, NSIndexPath *indexPath){
cell.textLabel.text = friend.name;
};
PPSArrayDatasource *datasources = [[PPSArrayDatasource alloc] initWithItems:self.friends cellIdentifier:@"friendCell" configureCellBlock:configCell];
self.tableView.dataSource = datasources;
}

那么在PPSArrayDatasource的items中装的应该就是一个一个的数组了,分别对应的每个section,这里我只是针对这种情况说明一下,不管是多个section还是单个section都可以使用这种方法,来瘦身

在 table view controllers 之间共享。

这样的好处在于,你可以单独测试这个类,再也不用写第二遍。该原则同样适用于数组之外的其他对象。

将业务逻辑移到model中

下面是在viewcontroller中写的用来查找一个用户的目前的优先事项的列表:

1
2
3
4
5
6
7
- (void)loadPriorities {
NSDate* now = [NSDate date];
NSString* formatString = @"startDate = %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];
self.priorities = [priorities allObjects];
}

把这些代码移动到 User 类的 category 中会变得更加清晰,处理之后,在 View Controller.m 中看起来就是这样:

1
2
3
- (void)loadPriorities {
self.priorities = [user currentPriorities];
}

在 User+Extensions.m 中:

1
2
3
4
5
6
- (NSArray*)currentPriorities {
NSDate* now = [NSDate date];
NSString* formatString = @"startDate = %@";
NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}

有些代码不能被轻松地移动到 model 对象中,但明显和 model 代码紧密联系,对于这种情况,我们可以使用一个 Store:

创建store类

在一些情况下中,我们需要加载文件并解析它。下面就是 view controller 中的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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:0
error:NULL];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
_users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"];
_photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"];
[unarchiver finishDecoding];
}

但是 view controller 没必要知道这些,所以我们可以创建了一个 Store 对象来做这些事。通过分离,我们就可以复用这些代码,单独测试他们,并且让 view controller 保持小巧。Store 对象会关心数据加载、缓存和设置数据栈。它也经常被称为服务层或者仓库。

把网络层请求逻辑移到model层

和上面的主题相似:不要在 view controller 中做网络请求的逻辑。取而代之,你应该将它们封装到另一个类中。这样,你的 view controller 就可以在之后通过使用回调(比如一个 completion 的 block)来请求网络了。这样的好处是,缓存和错误控制也可以在这个类里面完成。

把View移到View层

不应该在 view controller 中构建复杂的 view 层次结构。你可以使用 Interface Builder 或者把 views 封装到一个 UIView 子类当中。例如,如果你要创建一个选择日期的控件,把它放到一个名为 DatePickerView 的类中会比把所有的事情都在 view controller 中做好好得多。再一次,这样增加了可复用性并保持了简单。

简单来说,就是将一个viewcontroller中复杂的view构造,放到一个单独的view类中,然在viewcontroller中,只需要构建一个这个类就行。

总结

我们已经看到一些用来创建更小巧的 view controllers 的技术。我们并不是想把这些技术应用到每一个可能的角落,只是我们有一个目标:写可维护的代码。知道这些模式后,我们就更有可能把那些笨重的 view controllers 变得更整洁。