在 iOS 5 之前涕滋,view controller 容器是 Apple 的特權(quán)睬辐。實際上,在 view controller 編程指南中還有一段申明宾肺,指出你不應該使用它們溯饵。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]
UIWindow 作為一個應用程序的根視圖(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)用手趣,要做好非常困難导街。
一個例子
當你還是個孩子漓库,在沙灘上玩時吧黄,你父母是否告訴過你柬帕,如果不停地用鏟子挖枚钓,最后會到達美國铅搓?我父母就說過,我就做了個叫做Tunnel的 demo 程序來驗證這個說法搀捷。你可以 clone 這個Github 代碼庫并運行這個程序星掰,它有助于讓你更容易理解示例代碼多望。(劇透:從丹麥西部開始,挖穿地球氢烘,你會到達南太平洋的某個地方)
為了尋找對跖點怀偷,也稱作相反的坐標,將拿著鏟子的小孩四處移動播玖,地圖會告訴你對應的出口位置在哪里椎工。點擊雷達按鈕,地圖會翻轉(zhuǎn)過來顯示位置的名稱蜀踏。
屏幕上有兩個 map view controllers维蒙。每個都需要控制地圖的拖動,標注和更新果覆。翻過來會顯示兩個新的 view controllers颅痊,用來檢索地理位置。所有的 view controllers 都包含于一個 parent view controller 中局待,它持有它們的 views斑响,并保證正確的布局和旋轉(zhuǎn)行為。
Root view controller 有兩個 container views燎猛。添加它們是為了讓布局恋捆,以及 child view controllers 的 views 的動畫做起來更容易,我們馬上就可以看到重绷。
- (void)viewDidLoad{? ? [superviewDidLoad];//Setup controllers_startMapViewController = [RGMapViewController new];? ? [_startMapViewController setAnnotationImagePath:@"man"];? ? [selfaddChildViewController:_startMapViewController];//? 1[topContainer addSubview:_startMapViewController.view];//? 2[_startMapViewController didMoveToParentViewController:self];//? 3[_startMapViewController addObserver:selfforKeyPath:@"currentLocation"options:NSKeyValueObservingOptionNewcontext:NULL];? ? _startGeoViewController = [RGGeoInfoViewController new];//? 4}
我們實例化了_startMapViewController沸停,用來顯示起始位置,并設(shè)置了用于標注的圖像昭卓。
_startMapViewcontroller被添加成 root view controller 的一個 child愤钾。這會自動在 child 上調(diào)用willMoveToParentViewController:方法。
child 的 view 被添加成 container view 的 subview候醒。
child 被通知到它現(xiàn)在有一個 parent view controller能颁。
用來顯示地理位置的 child view controller 被實例化了,但是還沒有被插入到任何 view 或 controller 層級中倒淫。
布局
Root view controller 定義了兩個 container views伙菊,它決定了 child view controller 的大小。Child view controllers 不知道會被添加到哪個容器中敌土,因此必須適應大小镜硕。
- (void) loadView{? ? mapView = [MKMapViewnew];? ? mapView.autoresizingMask =UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;? ? [mapView setDelegate:self];? ? [mapView setMapType:MKMapTypeHybrid];self.view = mapView;}
現(xiàn)在,它們就會用 super view 的 bounds 來進行布局返干。這樣增加了 child view controller 的可復用性兴枯;如果我們把它 push 到 navigation controller 的棧中,它仍然會正確地布局矩欠。
過場動畫
Apple 已經(jīng)針對 view controller 容器做了細致的 API财剖,我們可以構(gòu)造我們能想到的任何容器場景的動畫悠夯。Apple 還提供了一個基于 block 的便利方法,來切換屏幕上的兩個 controller views躺坟。方法transitionFromViewController:toViewController:(...)已經(jīng)為我們考慮了很多細節(jié)沦补。
- (void) flipFromViewController:(UIViewController*) fromController? ? ? ? ? ? ? toViewController:(UIViewController*) toController? ? ? ? ? ? ? ? ? withDirection:(UIViewAnimationOptions) direction{? ? toController.view.frame = fromController.view.bounds;//? 1[selfaddChildViewController:toController];//[fromController willMoveToParentViewController:nil];//[selftransitionFromViewController:fromController? ? ? ? ? ? ? ? ? ? ? toViewController:toController? ? ? ? ? ? ? ? ? ? ? ? ? ? ? duration:0.2options:direction |UIViewAnimationOptionCurveEaseInanimations:nilcompletion:^(BOOLfinished) {? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? [toController didMoveToParentViewController:self];//? 2[fromController removeFromParentViewController];//? 3}];}
在開始動畫之前,我們把toController作為一個 child 進行添加咪橙,并通知fromController它將被移除策彤。如果fromController的 view 是容器 view 層級的一部分,它的viewWillDisappear:方法就會被調(diào)用匣摘。
toController被告知它有一個新的 parent,并且適當?shù)?view 事件方法將被調(diào)用裹刮。
fromController被移除了音榜。
這個為 view controller 過場動畫而準備的便捷方法會自動把老的 view controller 換成新的 view controller。然而捧弃,如果你想實現(xiàn)自己的過場動畫赠叼,并且希望一次只顯示一個 view,你需要在老的 view 上調(diào)用removeFromSuperview违霞,并為新的 view 調(diào)用addSubview:嘴办。錯誤的調(diào)用次序通常會導致UIViewControllerHierarchyInconsistency警告。例如:在添加 view 之前調(diào)用didMoveToParentViewController:就觸發(fā)這個警告买鸽。
為了能使用UIViewAnimationOptionTransitionFlipFromTop動畫涧郊,我們必須把 children's view 添加到我們的 view containers 里面,而不是 root view controller 的 view眼五。否則動畫將導致整個 root view 都翻轉(zhuǎn)妆艘。
通信
View controllers 應該是可復用的、自包含的實體看幼。Child view controllers 也不能違背這個經(jīng)驗法則批旺。為了達到目的,parent view controller 應該只關(guān)心兩個任務(wù):布局 child view controller 的 root view诵姜,以及與 child view controller 暴露出來的 API 通信汽煮。它絕不應該去直接修改 child view tree 或其他內(nèi)部狀態(tài)。
Child view controller 應該包含管理它們自己的 view 樹的必要邏輯棚唆,而不是把它們看作單純呆板的 views暇赤。這樣,就有了更清晰的關(guān)注點分離和更好的可復用性瑟俭。
在示例程序 Tunnel 中翎卓,parent view controller 觀察了 map view controllers 上的一個叫currentLocation的屬性。
[_startMapViewController addObserver:selfforKeyPath:@"currentLocation"options:NSKeyValueObservingOptionNewcontext:NULL];
當這個屬性跟著拿著鏟子的小孩的移動而改變時摆寄,parent view controller 將新坐標的對跖點傳遞給另一個地圖:
[oppositeController updateAnnotationLocation:[newLocation antipode]];
類似地失暴,當你點擊雷達按鈕坯门,parent view controller 給新的 child view controllers 設(shè)置待檢索的坐標。
[_startGeoViewControllersetLocation:_startMapViewController.currentLocation];[_targetGeoViewControllersetLocation:_targetMapViewController.currentLocation];
我們想要達到的目標和你選擇的手段無關(guān)逗扒,從 child 到 parent view controller 消息傳遞的技術(shù)古戴,不論是采用 KVO,通知矩肩,或者是委托模式现恼,child view controller 都應該獨立和可復用。在我們的例子中黍檩,我們可以將某個 child view controller 推入到一個 navigation 棧中叉袍,它仍然能夠通過相同的 API 進行通信。