怎么优化ViewController,区分模块,分类管理


View controllers 一般是 iOS 项目中最大的文件,而且它们包含了许多没必要要的代码。因此 View controllers 中的代码几乎老是复用率最低的。咱们将会看到给 view controllers 瘦身的技术,让代码变得能够复用,以及把代码移动到更合适的地方。html

你能够在 Github 上获取关于这个问题的示例项目ios

把 Data Source 和其余 Protocols 分离出来

UITableViewDataSource 的代码提取出来放到一个单独的类中,是为 view controller 瘦身的强大技术之一。当你多作几回,你就能总结出一些模式,而且建立出可复用的类。git

举个例,在示例项目中,有个 PhotosViewController 类,它有如下几个方法:github

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

这些代码基本都是围绕数组作一些事情,更针对地说,是围绕 view controller 所管理的 photos 数组作一些事情。咱们能够尝试把数组相关的代码移到单独的类中。咱们使用一个 block 来设置 cell,也能够用 delegate 来作这件事,这取决于你的习惯。objective-c

@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 controller 中的这 3 个方法去掉了,取而代之,你能够建立一个 ArrayDataSource 类的实例做为 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;

如今你不用担忧把一个 index path 映射到数组中的位置了,每次你想把这个数组显示到一个 table view 中时,你均可以复用这些代码。你也能够实现一些额外的方法,好比 tableView:commitEditingStyle:forRowAtIndexPath:,在 table view controllers 之间共享。缓存

这样的好处在于,你能够单独测试这个类,不再用写第二遍。该原则一样适用于数组以外的其余对象。网络

在今年咱们作的一个应用里面,咱们大量使用了 Core Data。咱们建立了类似的类,但和以前使用的数组不同,它用一个 fetched results controller 来获取数据。它实现了全部动画更新、处理 section headers、删除操做等逻辑。你能够建立这个类的实例,而后赋予一个 fetch request 和用来设置 cell 的 block,剩下的它都会处理,不用你操心了。mvc

此外,这种方法也能够扩展到其余 protocols 上面。最明显的一个就是 UICollectionViewDataSource。这给了你极大的灵活性;若是,在开发的某个时候,你想用 UICollectionView 代替 UITableView,你几乎不须要对 view controller 做任何修改。你甚至可让你的 data source 同时支持这两个协议。app

将业务逻辑移到 Model 中

下面是 view controller(来自其余项目)中的示例代码,用来查找一个用户的目前的优先事项的列表:

- (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 中会变得更加清晰,处理以后,在 View Controller.m 中看起来就是这样:

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

User+Extensions.m 中:

- (NSArray*)currentPriorities {    NSDate* now = [NSDate date];    NSString* formatString = @"startDate = %@";
    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 保持小巧。Store 对象会关心数据加载、缓存和设置数据栈。它也常常被称为服务层或者仓库

把网络请求逻辑移到 Model 层

和上面的主题类似:不要在 view controller 中作网络请求的逻辑。取而代之,你应该将它们封装到另外一个类中。这样,你的 view controller 就能够在以后经过使用回调(好比一个 completion 的 block)来请求网络了。这样的好处是,缓存和错误控制也能够在这个类里面完成。

把 View 代码移到 View 层

不该该在 view controller 中构建复杂的 view 层次结构。你可使用 Interface Builder 或者把 views 封装到一个 UIView 子类当中。例如,若是你要建立一个选择日期的控件,把它放到一个名为 DatePickerView 的类中会比把全部的事情都在 view controller 中作好好得多。再一次,这样增长了可复用性并保持了简单。

若是你喜欢 Interface Builder,你也能够在 Interface Builder 中作。有些人认为 IB 只能和 view controllers 一块儿使用,但事实上你也能够加载单独的 nib 文件到自定义的 view 中。在示例程序中,咱们建立了一个 PhotoCell.xib,包含了 photo cell 的布局:

PhotoCell.xib screenshot

就像你看到的那样,咱们在 view(咱们没有在这个 nib 上使用 File's Owner 对象)上面建立了 properties,而后链接到指定的 subviews。这种技术一样适用于其余自定义的 views。

通信

其余在 view controllers 中常常发生的事是与其余 view controllers,model,和 views 之间进行通信。这固然是 controller 应该作的,但咱们仍是但愿以尽量少的代码来完成它。

关于 view controllers 和 model 对象之间的消息传递,已经有不少阐述得很好的技术(好比 KVO 和 fetched results controllers)。可是 view controllers 之间的消息传递稍微就不是那么清晰了。

当一个 view controller 想把某个状态传递给多个其余 view controllers 时,就会出现这样的问题。较好的作法是把状态放到一个单独的对象里,而后把这个对象传递给其它 view controllers,它们观察和修改这个状态。这样的好处是消息传递都在一个地方(被观察的对象)进行,并且咱们也不用纠结嵌套的 delegate 回调。这实际上是一个复杂的主题,咱们可能在将来用一个完整的话题来讨论这个主题。

总结

咱们已经看到一些用来建立更小巧的 view controllers 的技术。咱们并非想把这些技术应用到每个可能的角落,只是咱们有一个目标:写可维护的代码。知道这些模式后,咱们就更有可能把那些笨重的 view controllers 变得更整洁。

扩展阅读

相关文章
相关标签/搜索