轻量化ViewController
MVC最令人头疼的问题可能就是随着项目愈发复杂,ViewController的代码也会变得越来越冗长。阅读了objc的《Lighter View Controllers》和《Clean Table View Code》这两篇文章之后,总结了一些常用的轻量化ViewController的小技巧。
分离业务重点
既然要简化ViewController中的代码,那么在不改变原来实现方式的前提下,唯一的方法就是把一些可以不用放在ViewController中的代码转移出去。同时,也应该理解,MVC只是一种设计典范,并没有说一定只是由Model-View-Controller这三个文件组成,而所谓的Controller也可以是一组文件。
一些业务逻辑的转移
假设原来的ViewController中有一个方法叫做“loadPriorities”,用来给自己这个类的priorities赋值,
- (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中,从而把loadPriorities方法简化为:
- (void)loadPriorities { self.priorities = [user currentPriorities];
}
文件读写的转移
我们都有过读写plist或者其他文件的经历,一旦读写文件或者读取到的数据处理起来较为复杂,就可以专门建立一个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];
}
通过分离,我们就可以复用这些代码,单独测试他们,并且让 view controller 保持小巧。
Store 对象会关心数据加载、缓存和设置数据栈。它也经常被称为服务层或者仓库。
转移网络请求逻辑
和文件读写类似,网络请求的逻辑可以转移到一个单独的Manager类或者Model的Category中去。这样做,不仅可以简化ViewController并在ViewController中利用回调block来请求网络,也可以在单独的类中实现错误处理和缓存控制等相关问题。
ViewController之间的消息传递
ViewController与Model和View之间的信息交互已经在MVC设计模式中阐述得非常清楚了,但是ViewController和其他多个ViewController之间的信息传递却是一个无法回避也并不容易的事。
一个比较好的方案是把信息放到一个单独的对象里,然后把这个对象传递给其它 view controllers,它们观察和修改这个信息。这样的好处是消息传递都在一个地方(被观察的对象)进行,而且我们也不用纠结嵌套的 delegate 回调。
优化UITableViewController
UITableView在各个app中被大量的使用,同时也是ViewController简洁性的杀手。对于它的优化自然也是重中之重。
UITableViewController的优缺点
TableViewControllers 相对于标准ViewControllers 的一个特别的好处是它支持 Apple 实现的“下拉刷新”。目前,文档中唯一的使用 UIRefreshControl 的方式就是通过 table view controller 。虽然有诸多框架也实现了这一功能,但毕竟不是官方文档提供的框架,下一个版本的系统是否还支持并不好说。
TableViewControllers也有自己最大的缺点。它的 view 属性永远都是一个 table view。如果我们稍后决定在 table view 旁边显示一些东西(比如一个地图),如果不依赖于那些奇怪的技巧,估计就没什么办法了。
了解了TableViewControllers的优缺点后就应该根据实际需求选择比较合适的实现方法了。接下来就是几个优化table view的小技巧。
使用Child View Controller
一旦决定把UITableViewController作为Child View Controller,最重要的一点是在UITableViewController和Parent View Controller之间建立消息传递的渠道。比如用户选择了一个 table view 中的 cell,parent view controller 需要知道这个事件来推入其他 view controller。根据使用习惯,通常最清晰的方式是为这个 table view controller 定义一个 delegate protocol,然后到 parent view controller 中去实现。parent view controller中的代码如下:
@protocol DetailsViewControllerDelegate
- (void)didSelectPhotoAttributeWithKey:(NSString *)key;
@end
- (void)didSelectPhotoAttributeWithKey:(NSString *)key
{
DetailViewController *cOntroller= [[DetailViewController alloc] init];
controller.key = key;
[self.navigationController pushViewController:controller animated:YES];
}
分离DataSource
DateSource中的代上基本都是围绕数组做一些事情,更针对地说,是围绕 view controller(此时它自身作为table view的DataSource) 所管理的数组做一些事情。我们可以尝试把数组相关的代码移到单独的类中,与此同时,可以使用一个 block 来设置 cell,也可以用 delegate 来做这件事。
经过简化,ViewController中的代码现在看上去是这个样子的:
void (^configureCell)(Cell*, Model*) = ^(Cell* cell, Model* model) {
cell.label.text = model.name
}
modelsArrayDataSource = [[ArrayDataSource alloc] initWithItems:models
cellIdentifier:PhotoCellIdentifier configureCellBlock:configureCell]
self.tableView.dataSource = modelsArrayDataSource
传入modelsArrayDataSource的block主要是在cellForRowAtIndexPath方法中用来配置cell。
这样的好处在于,我们可以单独测试这个类,再也不用写第二遍。该原则同样适用于数组之外的其他对象。除此以外,这种方法还可以扩展到其他 protocols 上面。最明显的一个就是UICollectionViewDataSource。这给我们带来了极大的灵活性;如果,在开发的某个时候想用 UICollectionView 代替 UITableView,几乎不需要对 view controller 作任何修改。甚至可以让你的 data source 同时支持这两个协议。
在 Cell 内部控制 Cell 的状态
如果要修改Cell默认的选中或者高亮状态下的表现,可以通过代理去设置。
- (void)tableView:(UITableView *)tableView
didHighlightRowAtIndexPath:(NSIndexPath *)indexPath
{
PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}
但是更好的解决方案是对Controller隐藏Cell的实现细节,把这些实现细节放到Cell内部实现,而cell仅仅对外暴露一个接口。
@implementation Cell
- (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
总结
View controller 应该在 model 和 view 对象之间扮演协调者和调解者的角色。它不应该关心明显属于 view 层或 model 层的任务。我们应该始终记住这点,这样 delegate 和 data source 方法会变得更小巧,最多包含一些简单地样板代码。
这不仅减少了 table view controllers 那样的大小和复杂性,而且还把业务逻辑和 view 的逻辑放到了更合适的地方。Controller 层的里里外外的实现细节都被封装成了简单地 API,最终,它变得更加容易理解,也更利于团队协作。