这是前一段时间在伯乐在线翻译的一篇文章,个人理解讲的主要内容是cell的封装,也就是说通常情况下你可以在viewController把数据解析完成,然后对cell赋值,调整控件的位置。你也可以直接把数据模型赋值给cell,cell解析数据,根据数据调整自身的显示,这样看起来cell和UITableViewController能够更好的解耦。下面讲的就是后者这种情况。
在每个 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= [[SKContentViewControlleralloc] 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{
[cellremoveViewControllerFromParentViewController];
}
-(void)addViewControllerToParentViewController:(UIViewController*)parentViewController{
[parentViewControllercontentViewController];
[self.contentViewController didMoveToParentViewController:parentViewController];
[self.contentView addSubview:self.contentViewController.view];
}
-(void)removeViewControllerFromParentViewController{
[self.contentViewController.viewremoveFromSuperview];
[self.contentViewController willMoveToParentViewController:nil];
[self.contentViewControllerremoveFromParentViewController];
}
在视图控制器作为子视图控制器加入之后,将 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= [[SKContentViewControlleralloc] initWithModel:model];
[self addChildContentViewController:contentViewController];
returncontentViewController;
}];
}
-(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;
returncell;
}
在 cell 里面,你可以拿到这个 hostedView
,将它加到子视图中,并且在重用的时候清除它。
-(void)setHostedView:(UIView*)hostedView{
_hostedView= hostedView;
[self.contentView addSubview:hostedView];
}
- (void)prepareForReuse{
[superprepareForReuse];
[self.hostedViewremoveFromSuperview];
self.hostedView= nil;
}
这就是使用简单方法时你要做的所有事情。为了将视图控制器用于重用,需要一个 NSMutableSet
类型的 unusedViewControllers
和一个NSMutableDictionary
类型的viewControllersByIndexPath
.
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{
UITableViewCell*cell =//make a cell
SKViewController*viewController = [selfrecycledOrNewViewController];
viewController.model= [self.dataSource objectAtIndexPath:indexPath];
self.viewControllersByIndexPath[indexPath]= viewController;
cell.hostedView= contentViewController.view;
returncell;
}
- (UIViewController*)recycledOrNewViewController{
if(self.unusedViewControllers.count> 1){
UIViewController*viewController = [self.unusedViewControllersanyObject];
[self.unusedViewControllers removeObject:viewController];
returnviewController;
}
SKViewController*contentViewController= [[SKContentViewControlleralloc] init];
[self addChildViewController:contentViewController];
returncontentViewController;
}
-(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= [NSMutableSetset];
}
return_unusedViewControllers;
}
- (NSMutableDictionary*)viewControllersByIndexPath{
if(!_viewControllersByIndexPath){
self.viewControllersByIndexPath= [NSMutableDictionarydictionary];
}
return_viewControllersByIndexPath;
}
有三件重要的事情:
第一,
unusedViewControllers
里面装的是所有等待重用的视图控制器;
第二,
viewControllersByIndexPaths
里面装的是所有正在用的视图控制器(我们必须持有它们,否则将会被销毁)。
最后,cell 只与视图控制器的
hostedView
接触,符合我们之前的视图不能知道视图控制器原则。
这是我发现的适用于将 UIViewController
对象放入cell中的两个最好方法。如果我漏掉了任何技术我很乐意倾听。