轉自:http://tang3w.com/translate/objective-c/objc.io/2013/10/28/view-controller-容器.html
注:這篇翻譯已經過 objc.io 授權,原文鏈接是:View Controller Containment
在 iOS 5 之前,view controller 容器只是 Apple 公司的一個福利。實際上,在 view controller 編程指南中還有一段申明,你不應該使用它們。Apple 對 view controllers 的總的建議是“一個 view controller 管理一個全屏幕的內容”。這個建議後來被改爲“一個 view controller 管理一個自包含的內容單元”。爲什麼 Apple 不想讓我們構建自己的 tab bar controllers 和 navigation controllers?或者更確切地說,這段代碼有什麼問題:
[viewControllerA.view addSubView:viewControllerB.view]
UIWindow 作爲作爲一個應用程序的根視圖(root view),是旋轉和初始佈局消息等事件產生的來源。在上圖中,child view controller 的 view 插入到 root view controller 的視圖層級中,被排除在這些事件之外了。View 事件方法諸如 viewWillAppear:
將不會被調用。
在 iOS 5 之前構建自定義的 view controller 容器時,要保存一個 child view controller 的引用,還要手動在 parent view controller 中轉發所有 view 事件方法的調用,要做好非常困難。
一個例子
當你還是個孩子,在沙灘上玩時,你父母是否告訴過你,如果不停地用鏟子挖,最後會到達中國?我父母就說過,我就做了個叫做 Tunnel 的 demo 程序來驗證這個說法。你可以 clone 這個 Github 代碼庫並運行這個程序,它有助於讓你更容易理解示例代碼。(劇透:從丹麥西部開始,挖穿地球,你會到達南太平洋的某個地方。)
爲了尋找對跖點,也稱作相反的座標,將拿着鏟子的小孩四處移動,地圖會告訴你對應的出口位置在哪裏。點擊雷達按鈕,地圖會翻轉過來顯示位置的名稱。
屏幕上有兩個 map view controllers。每個都需要控制地圖的拖動,標註和更新。翻過來會顯示兩個新的 view controllers,用來檢索地理位置。所有的 view controllers 都包含於一個 parent view controller 中,它持有它們的 views,並保證正確的佈局和旋轉行爲。
Root view controller 有兩個 container views。添加它們是爲了讓佈局,以及 child view controllers 的 views 的動畫做起來更容易,我們馬上就可以看到。
- (void)viewDidLoad
{
[super viewDidLoad];
//Setup controllers
_startMapViewController = [RGMapViewController new];
[_startMapViewController setAnnotationImagePath:@"man"];
[self addChildViewController:_startMapViewController]; // 1
[topContainer addSubview:_startMapViewController.view]; // 2
[_startMapViewController didMoveToParentViewController:self]; // 3
[_startMapViewController addObserver:self
forKeyPath:@"currentLocation"
options:NSKeyValueObservingOptionNew
context:NULL];
_startGeoViewController = [RGGeoInfoViewController new]; // 4
}
我們實例化了 _startMapViewController
,用來顯示起始位置,並設置了用於標註的圖像。
-
_startMapViewcontroller
被添加成 root view controller 的一個 child。這會自動在 child 上調用willMoveToParentViewController:
方法。 - child 的 view 被添加成 container view 的 subview。
- child 被通知到它現在有一個 parent view controller。
- 用來顯示地理位置的 child view controller 被實例化了,但是還沒有被插入到任何 view 或 controller 層級中。
佈局
Root view controller 定義了兩個 container views,它決定了 child view controller 的大小。Child view controllers 不知道會被添加到哪個容器中,因此必須適應大小。
- (void) loadView
{
mapView = [MKMapView new];
mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[mapView setDelegate:self];
[mapView setMapType:MKMapTypeHybrid];
self.view = mapView;
}
現在,它們就會用 super view 的 bounds 來進行佈局。這樣增加了 child view controller 的可複用性;如果我們把它 push 到 navigation controller 的棧中,它仍然會正確地佈局。
過場動畫
Apple 已經針對 view controller 容器做了細粒度很好的 API,我們可以構造我們能想到的任何容器場景的動畫。Apple 還提供了一個基於 block 的方便的方法,來切換屏幕上的兩個 controller views。方法 transitionFromViewController:toViewController:(...)
已經爲我們考慮了很多細節。
- (void) flipFromViewController:(UIViewController*) fromController
toViewController:(UIViewController*) toController
withDirection:(UIViewAnimationOptions) direction
{
toController.view.frame = fromController.view.bounds; // 1
[self addChildViewController:toController]; //
[fromController willMoveToParentViewController:nil]; //
[self transitionFromViewController:fromController
toViewController:toController
duration:0.2
options:direction | UIViewAnimationOptionCurveEaseIn
animations:nil
completion:^(BOOL finished) {
[toController didMoveToParentViewController:self]; // 2
[fromController removeFromParentViewController]; // 3
}];
}
-
在開始動畫之前,我們把
toController
作爲一個 child 進行添加,並通知fromController
它將被移除。如果fromController
的 view 是容器 view 層級的一部分,它的viewWillDisapear:
方法就會被調用。 -
toController
被告知它有一個新的 parent,並且適當的 view 事件方法將被調用。 -
fromController
被移除了。
這個爲 view controller 過場動畫而準備的便捷方法會自動把老的 view controller 換成新的 view controller。然而,如果你想實現自己的過場動畫,並且希望一次只顯示一個 view,你需要在老的 view 上調用 removeFromSuperview
,併爲新的
view 調用 addSubview:
。錯誤的調用次序通常會導致 UIViewControllerHierarchyInconsistency
警告。例如:在添加
view 之前調用didMoveToParentViewController:
就會發生。
爲了能使用 UIViewAnimationOptionTransitionFlipFromTop
動畫,我們必須把
children's view 添加到我們的 view containers 裏面,而不是 root view controller 的 view。否則動畫將導致整個 root view 都翻轉。
消息傳遞
View controllers 應該是可複用的、自包含的實體。Child view controllers 也不能違背這個經驗法則。爲了達到目的,parent view controller 應該只關心兩個任務:佈局 child view controller 的 root view,以及與 child view controller 暴露出來的 API 通信。它絕不應該去直接修改 child view tree 或其他內部狀態。
Child view controller 應該包含管理它們自己的 view 樹的必要邏輯,不要讓他們成爲呆板的 views。這樣,就有了更清晰的關注點分離和更好的可複用性。
在示例程序 Tunnel 中,parent view controller 觀察了 map view controllers 上的一個叫currentLocation
的屬性。
[_startMapViewController addObserver:self
forKeyPath:@"currentLocation"
options:NSKeyValueObservingOptionNew
context:NULL];
當這個屬性跟着拿着鏟子的小孩的移動而改變時,parent view controller 將新座標的對跖點傳遞給另一個地圖:
[oppositeController updateAnnotationLocation:[newLocation antipode]];
類似地,當你點擊雷達按鈕,parent view controller 給新的 child view controllers 設置待檢索的座標。
[_startGeoViewController setLocation:_startMapViewController.currentLocation];
[_targetGeoViewController setLocation:_targetMapViewController.currentLocation];
和你選擇的從 child 到 parent view controller 消息傳遞的技術(KVO,通知,或者委託模式)相獨立地,我們的目標都是一樣的:child view controller 應該獨立和可複用。在我們的例子中,我們可以將某個 child view controller 推入到一個 navigation 棧中,但仍然可以通過相同的 API 進行消息傳遞。