在 iOS 5 之前,view controller 容器是 Apple 的特權(quán)。實際上,在 view controller 編程指南中還有一段申明,指出你不應(yīng)該使用它們。Apple 對 view controllers 的總的建議曾經(jīng)是“一個 view controller 管理一個全屏幕的內(nèi)容”。這個建議后來被改為“一個 view controller 管理一個自包含的內(nèi)容單元”。為什么 Apple 不想讓我們構(gòu)建自己的 tab bar controllers 和 navigation controllers?或者更確切地說,這段代碼有什么問題:
[viewControllerA.view addSubView:viewControllerB.view]
http://wiki.jikexueyuan.com/project/objc/images/1-2.png" alt="Inconsistent view hierarchy" />
UIWindow 作為一個應(yīng)用程序的根視圖(root view),是旋轉(zhuǎn)和初始布局消息等事件產(chǎn)生的來源。在上圖中,child view controller 的 view 插入到 root view controller 的視圖層級中,被排除在這些事件之外了。View 事件方法諸如 viewWillAppear: 將不會被調(diào)用。
在 iOS 5 之前構(gòu)建自定義的 view controller 容器時,要保存一個 child view controller 的引用,還要手動在 parent view controller 中轉(zhuǎn)發(fā)所有 view 事件方法的調(diào)用,要做好非常困難。
當(dāng)你還是個孩子,在沙灘上玩時,你父母是否告訴過你,如果不停地用鏟子挖,最后會到達(dá)美國?我父母就說過,我就做了個叫做 Tunnel 的 demo 程序來驗證這個說法。你可以 clone 這個 Github 代碼庫并運行這個程序,它有助于讓你更容易理解示例代碼。(劇透:從丹麥西部開始,挖穿地球,你會到達(dá)南太平洋的某個地方)
http://wiki.jikexueyuan.com/project/objc/images/1-3.png" alt="Tunnel screenshot" />
為了尋找對跖點,也稱作相反的坐標(biāo),將拿著鏟子的小孩四處移動,地圖會告訴你對應(yīng)的出口位置在哪里。點擊雷達(dá)按鈕,地圖會翻轉(zhuǎn)過來顯示位置的名稱。
屏幕上有兩個 map view controllers。每個都需要控制地圖的拖動,標(biāo)注和更新。翻過來會顯示兩個新的 view controllers,用來檢索地理位置。所有的 view controllers 都包含于一個 parent view controller 中,它持有它們的 views,并保證正確的布局和旋轉(zhuǎn)行為。
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,用來顯示起始位置,并設(shè)置了用于標(biāo)注的圖像。
_startMapViewcontroller 被添加成 root view controller 的一個 child。這會自動在 child 上調(diào)用 willMoveToParentViewController: 方法。Root view controller 定義了兩個 container views,它決定了 child view controller 的大小。Child view controllers 不知道會被添加到哪個容器中,因此必須適應(yīng)大小。
- (void) loadView
{
mapView = [MKMapView new];
mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[mapView setDelegate:self];
[mapView setMapType:MKMapTypeHybrid];
self.view = mapView;
}
現(xiàn)在,它們就會用 super view 的 bounds 來進(jìn)行布局。這樣增加了 child view controller 的可復(fù)用性;如果我們把它 push 到 navigation controller 的棧中,它仍然會正確地布局。
Apple 已經(jīng)針對 view controller 容器做了細(xì)致的 API,我們可以構(gòu)造我們能想到的任何容器場景的動畫。Apple 還提供了一個基于 block 的便利方法,來切換屏幕上的兩個 controller views。方法 transitionFromViewController:toViewController:(...) 已經(jīng)為我們考慮了很多細(xì)節(jié)。
- (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 進(jìn)行添加,并通知 fromController 它將被移除。如果 fromController 的 view 是容器 view 層級的一部分,它的 viewWillDisapear: 方法就會被調(diào)用。toController 被告知它有一個新的 parent,并且適當(dāng)?shù)?view 事件方法將被調(diào)用。fromController 被移除了。這個為 view controller 過場動畫而準(zhǔn)備的便捷方法會自動把老的 view controller 換成新的 view controller。然而,如果你想實現(xiàn)自己的過場動畫,并且希望一次只顯示一個 view,你需要在老的 view 上調(diào)用 removeFromSuperview,并為新的 view 調(diào)用 addSubview:。錯誤的調(diào)用次序通常會導(dǎo)致 UIViewControllerHierarchyInconsistency 警告。例如:在添加 view 之前調(diào)用 didMoveToParentViewController: 就觸發(fā)這個警告。
為了能使用 UIViewAnimationOptionTransitionFlipFromTop 動畫,我們必須把 children's view 添加到我們的 view containers 里面,而不是 root view controller 的 view。否則動畫將導(dǎo)致整個 root view 都翻轉(zhuǎn)。
View controllers 應(yīng)該是可復(fù)用的、自包含的實體。Child view controllers 也不能違背這個經(jīng)驗法則。為了達(dá)到目的,parent view controller 應(yīng)該只關(guān)心兩個任務(wù):布局 child view controller 的 root view,以及與 child view controller 暴露出來的 API 通信。它絕不應(yīng)該去直接修改 child view tree 或其他內(nèi)部狀態(tài)。
Child view controller 應(yīng)該包含管理它們自己的 view 樹的必要邏輯,而不是把它們看作單純呆板的 views。這樣,就有了更清晰的關(guān)注點分離和更好的可復(fù)用性。
在示例程序 Tunnel 中,parent view controller 觀察了 map view controllers 上的一個叫 currentLocation 的屬性。
[_startMapViewController addObserver:self
forKeyPath:@"currentLocation"
options:NSKeyValueObservingOptionNew
context:NULL];
當(dāng)這個屬性跟著拿著鏟子的小孩的移動而改變時,parent view controller 將新坐標(biāo)的對跖點傳遞給另一個地圖:
[oppositeController updateAnnotationLocation:[newLocation antipode]];
類似地,當(dāng)你點擊雷達(dá)按鈕,parent view controller 給新的 child view controllers 設(shè)置待檢索的坐標(biāo)。
[_startGeoViewController setLocation:_startMapViewController.currentLocation];
[_targetGeoViewController setLocation:_targetMapViewController.currentLocation];
我們想要達(dá)到的目標(biāo)和你選擇的手段無關(guān),從 child 到 parent view controller 消息傳遞的技術(shù),不論是采用 KVO,通知,或者是委托模式,child view controller 都應(yīng)該獨立和可復(fù)用。在我們的例子中,我們可以將某個 child view controller 推入到一個 navigation 棧中,它仍然能夠通過相同的 API 進(jìn)行通信。