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