更轻的视图控制器(Lighter View Controllers)

原文地址:Lighter View Controllers - Lighter View Controllers - objc.io issue #1

更轻的视图控制器

作者: Chris Eidhof

译者: Yale.Zheng

ViewController 通常是iOS工程中最臃肿的文件,在大多数情况下它们都会负责很多不必要的工作。一般情况下,试图控制器在都是工程就最难以被复用的模块。为了解决这种状况,在本文中我们会展示一些方法来精简 ViewController 的代码,使得这部分代码的复用性更强。并且把不必要的代码迁移到更合适的地方。

文章相关的 示例工程 已经放在了 Github 上。

分离数据源(Data Source)和其它协议(Protocol)

精简 ViewController 的有效方法之一就是实现 UITableViewDataSource 协议相关的代码封装成一个类(比如本文中的 ArraryDataSource )。如果你经常在 UIViewController 中实现 UITableViewDataSource 协议,你会发现相关代码看起来都差不多。

举个例子,示例工程中的类 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;
}

可以看到上面方法的实现都与 NSArray 有关,还有一个方法的实现与 Photo 有关(PhotoCell 呈一一对应关系)。下面让我们来把与 NSArray 相关的代码从 ViewController 中抽离出来,并改用 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

现在你可以把 ViewController 中的相关方法移除,并且把 ViewControllerdataSource 设置为 ArrayDataSource 的实例。

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;

通过上面的方法,你就可以把设置 Cell 视图的工作从 ViewController 中抽离出来。现在你不需要再关心indexPath如何与 NSArrary 中的元素如何关联,当你需要将数组中的元素在其它 UITableView 中展示时你可以重用以上代码。你也可以在 * ArrayDataSource* 中实现更多的方法,比如tableView:commitEditingStyle:forRowAtIndexPath:

除了以上好处之外,我们还可以针对这部分实现编写单独的单元测试,而且再也不需要到处复制粘贴了。当你使用其他数据容器时,你可以用类似的方式来达到代码复用的效果。

该技巧同样适用于其他 Protocol ,比如 UICollectionViewDataSource 。通过该协议,你可以定义出各种各样的 UICollectionViewCell 。假如有一天,你需要在代码在使用到 UICollectionView 来替代当前的 UITableView,你只需要修改几行 ViewController 中的代码即可完成替换。你甚至能够让你的 DataSource 类同时实现 UICollectionViewDataSource 协议和 UITableViewDataSource 协议。

把业务逻辑移至 Model

下面是一段位于 ViewController 中的代码,作用是找出针对用户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];
}

然而,假如你把代码实现移至 UserCategory 中,代码将会更简洁、更清晰。

VIewController.m
- (void)loadPriorities {
self.priorities = [self.user currentPriorities];
}

User+Extension.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

在第一的版本的实例工程中,我们有一段用于从本地文件加载数据并解析的代码。这些代码位于 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 中抽离出来,我们可以对其复用、对其单独测试、让 ViewController 变得更轻巧( Store 对象一般负责数据的加载、缓存、持久化。Store 对象也经常被称作 Service Layer 对象,或者 Repository 对象)。

Web Service 逻辑移至 Model

这与上一个主题非常相似:别把 Web Service 相关的代码写在 ViewController 中,应该把这部分代码抽离出来。并通过方法的回调对数据进行处理。

不仅如此,你还可以把处理异常情况的工作也转交给 Store 对象负责。

把视图相关的代码移至 View

理论上讲,不应该把构建复杂视图的代码写在 View Controller 中,应该在 Interface Builder 或者封装一个 Vew 的子类来完成这部分工作。打个比方,你现在需要实现自定义一个日期选择器。你应该新建一个 DatePickerView 的子类来完成构建视图的工作,而不是把这部分工作放在 View Controller 中完成。同样的,这将是你的代码更简洁,复用性更强。

除了用 code 的形式来实现自定义视图,你也可以使用 Interface Builder 来完成构建自定义视图的工作。很多人都认为 Interface Builder 只能用于为 View Controller 构建视图,其实不然,你可以通过单独的 nib 文件来加载在 Interface Builder中构建的自定义视图。在示例工程当中,我们创建了一个包含了 Photo Cell 视图的 PhotoCell.xib 文件。

如你所见,我们在 view 中创建了属性(无需设置 File’s Owner 对象)并把它们与 Interface Builder 中的视图关联起来。这个方法同样适用于构建其它自定义视图。

通讯

我们在ViewController 中经常做的一件事就是与其它 ViewControllerModelView 进行通讯。虽然这本来就是 ViewController 应该负责的工作,但是我们可以通过一些方法来精简 ViewController 中的代码。

现在已经有很多成熟的方案来建立 ViewControllerView 的通讯(例如 KVOfetched results controllers)。然而 VIewController 之间的通讯目前还没有类似的方案可以借鉴。

在实际开发中,我们经常需要把 由一个 ViewControlller 持有的一些信息,传递到 多个 ViewController。通常,最好把这些信息独立封装成一个类。这样子的好处是所有的修改和读取都集中在一个对象,

结论

我们已经展示了一些创建更轻巧的 ViewController 的方法。我们并不奢求这些技术在任何请情况下都适用,我们只有一个目标:使得代码更易于维护。通过了解这些方法,我们能够更好的处理好复杂的视图控制器,并且让这些视图控制器的代码更整洁,更清晰。

你可能感兴趣的:(更轻的视图控制器(Lighter View Controllers))