[翻译]本文翻译自objc.io创始人、iOS大神Chris Eidhof的文章, 原文链接可查看Lighter View Controllers。
在iOS工程中,ViewController(界面控制器)通常是代码量最大的源码文件。它往往包含大量不必要的代码,而且当中的代码在绝大多数情况下都无法复用。今天我们来探究一些行之有效的方法,使得相关代码可以移动到更合适的地方,使其既能复用,控制器又能“瘦身”。关于本文内容的示例工程可以在GitHub下载。
分离数据源方法和其他协议方法
最强大的一项ViewController瘦身技术就是将UITableViewDataSource的代理方法转移到一个自定义类中。只要多次使用,你就会开始理解这个模式并创建相应的可复用类。
在本文的示例工程中, 有一个类叫PhotosViewController,它实现了以下方法:
- (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;
}
以上大量代码和数组相关,而且其中某些代码还和ViewController管理的图片数组photos直接关联。我们尝试将和数组相关的代码转移到一个自定义类中去。我们使用一个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
现在控制器中原来的三个方法就可以移走了,取而代之的,你只需要创建这个对象的实例并将其设置为tableview的数据源。
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;
现在你不再需要担心如何将一个indexPath映射到array的具体某一个元素上了,而且每次你要在一个tableView中展示一个数组中的数据, 你都可以复用这段代码。你还可以实现其他的DataSource方法使其在你所有的tableView控制器中可以共用,例如 tableView:commitEditingStyle:forRowAtIndexPath:
方法。
这样做还有一个好处,就是我们可以单独 测试这个类,而且再也不用担心需要重写一遍。除了数组以外的其他数据源,也适用同样的原则。
在今年的一个公司项目中,我们使用了大量的CoreData。我们创建了一个类似的类,但它的数据源不是一个数组,而是一个结果控制器。它实现了动画更新、section头部配置、删除的所有逻辑。有了这个类后,你只需要在控制器中创建一个它的实例对象,并赋值一个网络请求和block来配置cell,之后的逻辑将全部由它负责完成。
这种方法还可以延伸到其他协议中,一个典型的例子就是UICollectionViewDataSource。这就给你提供了巨大的灵活性,在你开发的某个阶段,当你决定使用UICollectionView来替代原先的UITableView时, 你几乎不需要改动ViewController中的任何代码。你甚至还可以让你的dataSource类同时支持两种协议。
转移“领域逻辑”到实体(Model)层
以下这段代码写在一个ViewController中(来自其他工程),用于筛选出User类中活跃列表。
- (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类的一个类拓展中,ViewController就会显得简洁很多,如下所示:
- (void)loadPriorities {
self.priorities = [self.user currentPriorities];
}
User+Extensions.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];
}
有些代码和实体的关联性强,但又无法简单地转移到一个Model类中,这时我们可以使用一个存储(Store)类。
创建存储(Store)类
在示例工程的第一版中有一些代码是关于从文件中加载并解析数据的。这段代码被写在了ViewController中:
- (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];
}
ViewController其实并不需要知道这段代码的逻辑。我们此时就可以创建一个Store对象来实现它。通过把这段代码分离出去,我们可以重复使用它、可以单独测试它、还能令我们的ViewController体量更小。这个Store类可以负责数据的加载、缓存,以及创建数据库。这个Store类通常也叫做service层或者仓库(repository)。
转移网络服务逻辑到实体(Model)层
这一点和上一点很类似:即不要在你的ViewController中实现网络服务逻辑。你应该将其封装到一个不同的类中,之后你的ViewController就可以调用这个类中的网络方法,并通过一个回调执行之后的逻辑(例如一个叫completion的block)。这样做的好处是你可以在这个类中实现所有的缓存工作和错误处理。
转移视图代码到视图(View)层
ViewController中不应该存在复杂结构的视图(Views)构建代码。要么你就用Interface builder,要么你就把这个View封装起来。举个例子,如果你要构建一个自定义的日期选择器,比起将代码写到ViewController中, 还不如写到一个叫DatePickerView的类中更合理。而且该做法令代码更容易使用、也更好重复使用。
如果你喜欢使用Interface Builder,你也可以在Interface Builder中实现这段逻辑。有些人会以为这种方法只能应用到ViewController中,但实际上你也可以通过独立的nib文件加载自定义Views。在我们的示例app中,我们创建了一个 PhotoCell.xib文件,它包含了photoCell的完整布局。
正如你所见的,我们在这个View上创建了两个label标签,并将其关联到一个的类中(在这个xib中我们并不使用File’s Owner对象)。这项技术对于其他自定义视图来说也相当容易上手。
控制器通信问题
在ViewController中,我们经常要处理它和其他ViewController、model和view之间的通信问题。这虽然正是一个控制器应该做的事,但我们也希望用尽可能少的代码实现它。
视图控制器和模型对象之间的通信有很多成熟的技术(例如KVO和结果控制器),但是视图控制器之间的通信代码往往不太清晰。
我们经常遇到这样的问题:一个视图控制器具有某种状态并与多个其他视图控制器通信。 通常情况下,将这个状态放入一个单独的对象并传递给其他视图控制器是合理的,然后其他视图控制器就可以观察并修改该状态。 这样做的好处是这个状态放在同一个地方,不至于最终让这段逻辑在嵌套的代理或者回调中“苦苦纠缠”。 但这是一个复杂的问题,未来我们可能提出一个课题专门研究它。
总结
我们已经学到了一些创建较小视图控制器的技巧。 但我们不应该为了使用这些技巧而使用,因为使用这些技术只有一个目标:编写可维护的代码。 通过了解这些模式,我们才更有可能用清晰的代码结构去编写复杂的视图控制器。