轻量级的视图控制器

原文链接:http://www.objc.io/issue-1/lighter-view-controllers.html

视图控制器经常是iOS工程里最大的文件,他们经常包含了很多不必要的代码。视图控制器基本上是代码里重用最少的部分。我们将着眼于是视图控制器瘦身相关的技术,来达到使代码可重用并且将代码移动到更合适的地方。

示例代码已经在GitHub上了。

将数据源和其他协议分开

使视图控制器瘦身最重要的一个技术是将UITableViewDataSouce的部分代码移动到它自己的类中。如果你经常这样干,你会发现一些模式并且写出可重用的类。

例如,在我们的示例代码中,有一个叫PhotosViewController的类有下面的方法:

# 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;
}

这里大部分的代码都是在跟数组打交道,其中一些试用了视图控制器管理的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

在视图控制器中这三个方法代码现在可以删除了,取而代之的是你要创建一个这个对象的实例,并且将它设为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中都可以重用这部分代码。你也可以实现如tableView:commitEditinggStyle:forRowAtIndexPath:等方法来实现table view中的代码共享。

还有一个好处就是我们可以单独测试这个类而不需要担心将这些代码在写一遍。同样的规则也适用于其他非数组类型的数据。

在我们今年工作的一个项目中,我们非常依赖Core Data。我们创建了一个相似的类,用的是fetched results controller而不是数组。它实现了更新动画、设置section header以及删除等逻辑。你可以创建一个它的实例并且传给它一个fetch request和一个用来设置cell的block,剩下的工作就交给它了。

此外,这个方法也可以延伸到协议。UICollectionViewDataSource是比较明显的一个。如果在开发过程中你决定使用UICollectionView而不是UiTableView的时候,这会给你带来极大的灵活性,你基本上不需要修改任何一处视图控制器里的代码。你甚至可以使你的data source支持协议。

将域逻辑移动到模型里

下面是一段视图控制器里的示例代码(来自另一个工程),这段代码的作用是为用户找到一个活跃优先级列表:

- (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里看起来会更简洁,像下面这样:

- (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里,但是我们依然可以通过存储将它跟模型联系在一起。

创建存储类

在示例代码的第一个版本中,有一些代码是用来从文件中导入数据并且解析,这些代码在视图控制器中:

- (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];
}

视图控制器不应该知道这些的。所以我们创建了一个存储对象来做这些。通过将它分离出来,我们可以重用这些代码,对它单独进行测试并且使视图控制器变得小巧。存储可以用来数据导入、缓存以及建立数据库栈。这个存储通常也叫做服务层或者仓库。

将Web Service逻辑挪到模型层

这个有点像我们上面讲到的话题:不要在视图控制器里做web service逻辑。相反,将他们封装在不同的类中。你的视图控制器可以调用这个类中的方法,带上一个回调处理(比如一个block)。你可以在这个类中做缓存和错误处理。

将视图相关的代码挪到视图层

复杂的视图层次不应该在视图控制器中构建。要么使用IB,要么将view封装到UiView的子类中。例如,你想创建一个你自己的日期选择控件,创建一个UIDatePickerView的子类比在视图控制器中做所有事情更说得通。同样,这也可以提高可重用性和简洁性。

如果你喜欢IB,你也可以在iB中做这些。有些人会认为只能在视图控制器中使用IB,但其实你也可以在自定义view中单独导入nib文件。在我们示例应用中,我们创建了一个PhotoCell.xib用来管理photo cell的布局。


就像你看到的那样,我们在view中创建属性(我们不适用xib中的File's Owner对象)并且将这些属性跟相应的子控件连接起来。这个技术也非常适用于其他自定义控件。

通信

视图控制器做的最多的事情就是跟其他的视图控制器、模型和控件通信。这也的确是一个控制器应该要做的。这是我们要用最少代码达到的目标。

有很多比较好的技术(例如KVO和fetched results controllers)可以用来在视图控制器和模型对象之间通信。但是,视图控制器之间的通信看起来就没有那么清晰。

我们经常会遇到这样的情况广,一个视图控制器有一些状态需要跟其他的几个视图控制器之间进行通信。一般情况下,我们可能会将这些状态封装成一个对象然后在几个视图控制器之间进行传递,然后所有的视图控制器都保留并且修改这个状态。这样的好处是我们最后不用纠结于嵌套代理回调。这是一个复杂的话题,将来我们可能用整个issue来处理它。

结论

我们已经见识了一些使视图控制器小巧的技术。我们不致力于将这些技术用在所有可用的地方,因为我们的目标是:写出可维护的代码。了解这些模式以后,我们有很多机会用来使视图控制器的代码更清晰。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值