打造轻量化的View Controller
转自
本文由破船译自objc
小引
很早以前就看到了这篇文章,该文是
Lighter View Controllers中比较重要的一篇,来自http://www.objc.io/,该站点的目标是致力于介绍Objective-C中最佳的实践技能和高级技术,以期刊的形式发表,每期一个主题,第一期就是以Lighter View Controllers为主。共有5篇文章,由于老破船精力有限,可能不会全部翻译,感兴趣的读者可以前往官方站点,进行阅读,千万别苦等破船进港时,万一等到花谢草枯了,可不好哟。
本文目录如下:
简介
剥离Data Source和其它Protocols
将业务逻辑移至Model Layer
创建Store类
将Web Service逻辑移至Model Layer
将View代码移至View Layer
与别的对象进行通讯
小结
延伸阅读
简介
在iOS工程中,view controllers经常是最大的文件,引起这样的主要原因是开发者在view controllers中编写了大量非必须代码,实际上,view controllers可以重用其中的许多代码。下面我们就来看看,有什么好的办法可以对view controllers进行瘦身,加强代码的可重用性(reusable),并将代码放到适当的地方。
提醒:本文涉及到的
示例工程已经放到GitHub上了。
剥离Data Source和其它Protocols
对view controllers瘦身最佳的方法之一就是将UITableViewDataSource涉及到的代码从view controllers中抽取出来,并封装到自己的一个类中。如果不止在一个view controllers中使用到UITableViewDataSource,那么会提高封装出来这个类的可重用性。
译者注:此处有一个前提条件原作者没有说明,我们需要注意一下——这里的view controllers是使用到了UITableView。
下面我们来看一个例子,在上面给出的示例工程中,有一个类PhotosViewController,里面原本有如下3个方法:
- # 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;
- }
上面3个方法中都涉及到了数组,并且在最后一个方法中利用索引(indexPatha)给UITableViewCell指定了相应的图片(view controllers管理着这些图片资源)。下面我们就来试着把与数组相关的代码封装到我们自己的
一个类中。
如下代码所示,这里通过一个block来配置UITableViewCell,当然,也可以使用delegate对UITableViewCell进行配置,这主要取决于开发者。
- @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
有了上面这个自定义的类,我们就可以把view controllers中的那3个方法移除掉,并创建自定义类的一个示例对象,然后将其设置为table view的data source,如下代码所示。
- 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;
现在,当每次希望将数组中的内容显示到table view中时,不必再考虑如何将index path映射到数组中的准确位置了,只需要重新上面的代码即可。另外,我们也可以在自定义类中,实现另外的一些方法(例如tableView:commitEditingStyle:forRowAtIndexPath:),以共享给所有的table view controllers。
这也会带来一个好处:可以单独的对自定义的这个类进行测试,而不用担心要重新写一些测试代码。其实,如果我们写的代码跟这里的情况类似,那么也可以这样做。
今年我在工作中写的一个应用程序,大量使用了Core Data。我也创建了类似的类。该类实现了动画更新的所有逻辑,section header处理,以及相关删除操作等。你可以创建该类的一个示例,然后设置一下对其调用的方法,以及配置cell的一个block,剩下的任务就能自动处理了。
此外,上面介绍的这种方法可以延伸到别的protocols,这能够给程序开发中带来很大的灵活性。例如UICollectionViewDataSource,在开发过程中,如果希望用UICollectionView替换已有的UITableView,我们几乎不需要对view controllers做很大的改动,甚至还能使我们的data source同时支持两种protocols(UITableViewDataSource和UICollectionViewDataSource)。
将业务逻辑移至Model Layer
下面的示例代码(另外一个工程)位于view controller,作用是找出针对用户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中,会让代码更加清晰。此时,在View Controller.m文件中看起来应该是这样的:
- - (void)loadPriorities {
- self.priorities = [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对象中,但是,很明显这些代码与model是相关的,针对这样的情况,我们可以单独为其写一个类,例如下面的store类。
创建Store类
本文给出示例工程的第一版代码中,有一部分代码是用来从文件中加载数据,并对其进行解析的,这些代码是在view controller中:
- - (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中剥离出来,不仅可以对其重用和单独测试,另外还能对view controller瘦身。Store类专注于数据的加载、缓存,以及对数据库进行配置。这里的Store也经常叫做service layer或者repository。
将Web Service逻辑移至Model Layer
这里的方法实际上跟上面介绍的非常类似:不要在view controller中做web service逻辑处理,而是将相关的逻辑处理封装到不同的类中。然后我们的view controller通过callback handler(例如一个completion block)来调用这些类的方法。
这样做带来的一个好处就是我们可以方便在这些封装类中做缓存和错误处理。
将View代码移至View Layer
切记不要在view controller中构建复杂的view结构。可选方案是:要么利用interface builder,或者就是将view的构建封装到一个UIView子类中。例如,如果你要构建一个date picker控件,可以基于DatePickerView类来构建,而不要把所有构建逻辑都放入view controller中。这样不仅能增加控件的可重用性,还能让代码简单化。
如果你喜欢用interface builder,那么也同样可以在interface builder中做这些事情。我们的一些开发者可能认为只有view controller才能在interface builder中使用,其实我们可以通过加载一个单独的nib文件,来加载我们在nib文件中定制的view。在本文给出的示例工程中,我创建了一个PhotoCell.xib文件,该文件中定制了一个photo cell:
![](http://www.cocoachina.com/cms/uploads/allimg/130705/4196_130705105832_1.png)
如上图所示,我在这个view中创建了两个属性:photoDateLable和photoTitleLable(注意:在这里的xib文件中没有使用File’s Owner object),并将这两个属性连接到制定的subview中。
上面介绍的技巧同样可以方便的用于别的一些custom view。
与别的对象进行通讯
在view controller中与别的view controller、model和view通讯是非常频繁的。虽然这确实是由controller负责的,不过,我们还是希望用最少的代码来完成相关的事情。
目前已经有许多技术可以用于view controller和model对象之间的通讯(例如KVO),不过view controller之间通讯的技术貌似不太明朗。
我们可能会经常遇到这样的问题:一个view controller有许多状态需要与其它多个view controller进行勾兑。通常,需要把这些状态封装到一个单独的对象中,然后将其传送到对应的view controller中,在这些view controller中对这些状态进行观察并修改即可——这样带来的优点是所有的状态都在一个地方,开发者不用纠结于delegate的callback。
实际上,关于对象间的通讯是一个复杂的topic,在今后的文章中,我们可能会对其进行深度分析。
小结
上面我们学习了一些对view controller瘦身的技巧。这些技巧并不强求用与所有的地方,其实只有一个目标:编写可维护的代码。大家通过了解这些方法,可以知道有更多的方法来应对复杂的view controller,让其看起来更加清晰。
延伸阅读