[翻譯]Cell 中的檢視控制器
這是前一段時間在伯樂線上翻譯的一篇文章,個人理解講的主要內容是cell的封裝,也就是說通常情況下你可以在viewController把資料解析完成,然後對cell賦值,調整控制元件的位置。你也可以直接把資料模型賦值給cell,cell解析資料,根據資料調整自身的顯示,這樣看起來cell和UITableViewController能夠更好的解耦。下面講的就是後者這種情況。
英文出處: khanlou.com。
在每個 iOS 開發者的生涯中,總有一些時候想把一個檢視控制器放到一個 tableView
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];
}
在 cell 的類中,實現這些方法來生成簡單的檢視控制器容器。
-(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中的兩個最好方法。如果我漏掉了任何技術我很樂意傾聽。