轻量级视图控制器

本文翻译自:Lighter View Controllers

在iOS项目中视图控制器通常是最大的文件,它们通常包含许多非必要的代码。视图控制器几乎总是代码里最少被复用的部分。接下来我们将使用技巧使视图控制器瘦身、复用代码、并把代码放到适当的地方。

此问题的示例代码在GitHub上。

将数据源和其他协议分开

为控制器瘦身最强有力技巧之一是将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;
}

这些代码中多次用到了数组,其中一些特定用于视图控制器管理的照片。所以,我们试着将与数组相关的代码移动到它自己的类中。我们使用 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的数据源。

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:commitEditingStyle:forRowAtIndexPath:,并在table view controllers中共享这些代码。

最方便的是我们可以分开测试这个类,而且不用担心重写这些代码。如果你使用数组以外的其它东西,这个原则同样适用。

在我们今年所做的一款应用中,大量使用了Core Data。我们创建了一个类似的类,但并不是用数组作为支持,而是一个获取结果的视图控制器。它实现了所有更新动画的逻辑,并做了组头和删除。然后,你可以创建此对象的实例,并为其提供一个获取请求和一个用于设置 cell 的 block ,其余的事将会被处理的很好。

此外,这种方式也可以扩展到其它协议。显而易见的是UICollectionViewDataSource。这样将给你带来极大的便利,例如:如果在开发中的某个节点上你决定使用UITableView来代替UICollectionView,这时你会发现很难改变视图控制器中的代码。你甚至可以让你的数据源支持两种协议。

将业务逻辑放到Model中

这是一段从其它应用程序中拿来的在视图控制器中搜寻用户活跃优先级的实例代码。

- (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类的分类中会更加清晰,然后View Controller.m看起来是这个样子:

- (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 中,但仍然与模型代码相关联,为此,我们可以使用存储类:

创建缓存模型

在我们示例程序的第一个版本中,有些代码是从文件中加载数据并解析。这些代码在视图控制器中:

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

视图控制器本不应该知道这些,为此我们创建了一个存储类来做这些。通过将这些代码分离,我们可以复用这些代码、分别测试并让视图控制器更轻量。存储类可以关心数据的加载、缓存、设置数据库堆栈。这个存储类也经常被称为服务层或仓库。

将网络处理逻辑放到Model层

这与上述主题十分相似:不要在视图控制器中做网络处理逻辑。而是将它封装到不同的类中。你的视图控制器之后可以通过调用方法处理回调(例如:完成的block)。最棒的是你可以在这个类中执行所有缓存和错误处理。

将创建UI的代码放到View层

在视图控制器中不应该创建复杂的视图层次结构。可以使用 Interface Builder 或者将 UI 层封装到 UIView 子类中。例如,如果你创建自己的时间选择器,将其单独封装成 DatePickerView 比将代码写到视图控制器中更说得通。这样又增加了可重用性和简单性。

如果你喜欢用 Interface Builder ,也可以这么做。有些人认为只能将其用于视图控制器,但你也可以使用自定义的 view 加载单独的 nib 文件。在我们的示例程序中,我们创建了一个 PhotoCell.xib 来包含照片 cell 的布局。
image

正如你所看到的,我们为这个自定义 view 创建属性并将它们关联到特定的 UIView 子类上(在这个 xib 中我们并没有使用 File’s Owner 对象)。这个技巧对于其它自定义控件也十分便利。

通信

另外一件经常发生在视图控制器中事是与其它 Controller、Model、View之间的通信。虽然这是视图控制器应该做的事,也是我们想尽可能用少量代码去实现的。

这里有许多很好解释的技巧对于视图控制器和模型之间的通信(例如 KVO 和 捕获结果控制器),然而视图控制器之间的通信往往不太清楚。

我们经常遇到某个视图控制器有一些状态并与其它多个视图控制器通讯的问题。通常,将这个状态放到单独的对象中并将其传递给其它视图控制器然后观察和修改这些状态是说的通的。它的优点是状态被放到一个地方,我们最终不会陷入嵌套的委托回调。这是个复杂的问题,今后我们可能把这个问题全部归功于此。

结论

我们看到了一些让视图控制器变轻的技巧。但我们不会尽可能随时随地使用这些技巧,我们只有一个目标:编写可维护的代码。通过了解这些模式,我们有更多的机会避免笨重的视图控制器,并使其更清晰。

延伸阅读

原文地址:http://wanxudong.top/blog/2017/06/08/qing-liang-ji-shi-tu-kong-zhi-qi/
posted @ 2017-06-23 13:40  GnodUxn  阅读(277)  评论(0编辑  收藏  举报