在每个 iOS 开发者的生涯中,总有一些时候想把一个视图控制器放到一个 tableView 的 cell 中。因为这是一个有用的工具去处理我在视图控制器中的各种复杂视图及繁琐操作,而且很容易想象的一种情况是你想要将一些视图堆在另一些视图上面。另一个常见的应用场景是将 collectionView 放在 cell 里。理想情况下里面的 collectionView 拥有它自己的控制器,这样外面的 tableView 控制器不会受到关联视图和每个 collection view cell 数据的影响。
因为 UIKit 有很多 hook(钩子函数)方法,用于组件之间与幕后相互通信,我们需要确保使用UIViewController 容器 API管理子视图控制器,如果不这样做可能会不知所以地失败或者表现不正常。
在这篇文章中我主要谈论 tableView 和它们的 cell,但是这些方法也适用于 collectionView。
为了简单起见,让视图控制器是 cell 中的唯一内容。相比管理单个视图控制器的根视图,尝试去管理一堆常规的视图反而会产生不必要的复杂。使用一个视图控制器并且每个 cell 中仅有一个视图控制器,布局(在 cell 层级)像下面这样简单
self.viewController.view.frame = self.contentView.bounds;
视图控制器能够内在处理自身的布局。我们也可以把高度的计算也放视图控制器里面。
这里有两种实现方法:可以每个 cell 持有一个视图控制器,也可以在控制器层管理这些视图控制器。
每个 cell 都持有视图控制器
如果我们在每个 cell 中放一个视图控制器,我们可以在这个 cell 中懒加载它。
- (SKContentViewController *)contentViewController {
if (!_contentViewController) {
SKViewController *contentViewController = [[SKContentViewController alloc] init];
self.contentViewController = contentViewController;
}
return _contentViewController;
}
记住我们不是将这个视图控制器的根视图作为一个子视图加入到我们 cell 的 contentView。当在 -cellForRowAtIndexPath: 方法中需要配置这个 cell 时,我们可以将我们的 model 传入到这个控制器,然后它会根据最新的内容配置自己。由于这些 cell 是复用的,你的控制器必须设计为在任何时候只要它的 model 改变就会完全地重置它自己。
UITableView 给我们了 cell 显示前后和移除前后的 hooks。我们想要在这个时候将 cell 的视图控制器加到我们的父表格视图控制器并且把 cell 视图控制器的根视图加到 cell 上.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cell addViewControllerToParentViewController:self];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
[cell removeViewControllerFromParentViewController];
}
在 cell 的类中,实现这些方法来生成简单的视图控制器容器。
- (void)addViewControllerToParentViewController:(UIViewController *)parentViewController {
[parentViewController contentViewController];
[self.contentViewController didMoveToParentViewController:parentViewController];
[self.contentView addSubview:self.contentViewController.view];
}
- (void)removeViewControllerFromParentViewController {
[self.contentViewController.view removeFromSuperview];
[self.contentViewController willMoveToParentViewController:nil];
[self.contentViewController removeFromParentViewController];
}
在视图控制器作为子视图控制器加入之后,将 subview 加到视图中,确认 -viewWillAppear: 之类的方法能被正确的调用。tableView 的 willDisplayCell: 方法与显示方法(-viewWillAppear: 和 -viewDidAppear:)是对应的,并且 -didEndDisplayingCell: 方法与消失方法相对应,因此我们就可以在这些方法展示我们的容器了。
在父容器中持有视图控制器
每个 cell 有它自己的视图控制器能够正常工作,但是感觉有点怪异。在 Cocoa 的 MVC 模式中,模型和视图不应该知道它们所用的视图控制器,让一个 cell(实际上是一个 UIView)持有一个控制器违反了这个规则体系。为了解决这个问题,我们可以在表格视图控制器中,在父容器级别持有所有子视图控制器。
我们有两个方法来实现这个任务,(比较简单的方法是)我们可以为我们需要展示的表格中的每个 item 预生成一个视图控制器(和一个 view),或者在需要的时候生成视图控制器,然后循环使用它们,就像 UITableView 对 cell 的重用一样(这个比较困难)。首先从简单的方式开始,当 iPhone 刚出现的时候,设备受内存的限制不能为表格中的每行生成一个 view。现在我们的设备有更多的内存,所以如果当你只需要展示很少的行时可能不需要重用视图。
- (void)setupChildViewControllers {
self.contentViewControllers = [self.modelObjects arrayByTransformingObjectsUsingBlock:^id(SKModel *model) {
SKViewController *contentViewController = [[SKContentViewController alloc] initWithModel:model];
[self addChildContentViewController:contentViewController];
return contentViewController;
}];
}
- (void)addChildContentViewController:(UIViewController *)childController {
[self addChildViewController:childController];
[childController didMoveToParentViewController:self];
}
(上面我使用了 Objective-Shorthand 中的 -arrayByTransformingObjectsUsingBlock: 方法,也就是所谓的 -map:)
一旦你有了视图控制器,你可以在方法 -cellForRowAtIndexPath: 中把它们的视图放到 cell 里
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = //make a cell
SKViewController *viewController = self.contentViewControllers[indexPath.row];
cell.hostedView = contentViewController.view;
return cell;
}
在 cell 里面,你可以拿到这个 hostedView,将它加到子视图中,并且在重用的时候清除它。
- (void)setHostedView:(UIView *)hostedView {
_hostedView = hostedView;
[self.contentView addSubview:hostedView];
}
- (void)prepareForReuse {
[super prepareForReuse];
[self.hostedView removeFromSuperview];
self.hostedView = nil;
}
这就是简单的方法你要做的所有事情。为了将视图控制器用于重用,需要一个 NSMutableSet 类型的 unusedViewControllers 和一个 NSMutableDictionary 类型的viewControllersByIndexPath.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = //make a cell
SKViewController *viewController = [self recycledOrNewViewController];
viewController.model = [self.dataSource objectAtIndexPath:indexPath];
self.viewControllersByIndexPath[indexPath] = viewController;
cell.hostedView = contentViewController.view;
return cell;
}
- (UIViewController *)recycledOrNewViewController {
if (self.unusedViewControllers.count > 1) {
UIViewController *viewController = [self.unusedViewControllers anyObject];
[self.unusedViewControllers removeObject:viewController];
return viewController;
}
SKViewController *contentViewController = [[SKContentViewController alloc] init];
[self addChildViewController:contentViewController];
return contentViewController;
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UIViewController *viewController = self.viewControllersByIndexPath[indexPath];
[self.viewControllersByIndexPath removeObjectForKey:indexPath]
[self.unusedViewControllers addObject:viewController];
}
- (NSMutableSet *)unusedViewControllers {
if (!_unusedViewControllers) {
self.unusedViewControllers = [NSMutableSet set];
}
return _unusedViewControllers;
}
- (NSMutableDictionary *)viewControllersByIndexPath {
if (!_viewControllersByIndexPath) {
self.viewControllersByIndexPath = [NSMutableDictionary dictionary];
}
return _viewControllersByIndexPath;
}
有三件重要的事情,一,unusedViewControllers 里面装的是所有等待重用的视图控制器,二,viewControllersByIndexPaths 里面装的是所有正在用的视图控制器(我们必须持有它们,否则将会被销毁)。最后,cell 只与视图控制器的 hostedView 接触,符合我们之前的视图不能知道视图控制器原则。
这是我发现的适用于将 UIViewController 对象放入cell中的两个最好方法。如果我漏掉了任何技术我很乐意倾听。
英文出处:khanlou.com
本文由 伯乐在线 - Above 翻译,nathanw 校稿
译文链接:http://ios.jobbole.com/82123/