容器視圖控制器是一種結合多個視圖控制器的內容到一個單一的用戶界面上的方式。容器視圖控制器經常被用來使導航更方便,基於已經存在的內容創建一個新的用戶界面類型。例如,在UIKit中的容器視圖控制器包括UINavigationcontroller,UITabBarcontroller 和 UISplitViewcontroller,它們都可以使用戶界面在不同視圖部分之間的切換和導航更加的容易。
設計一個自定義的容器視圖控制器
在幾乎所有的方面,一個容器視圖控制器就像其它任何一個內容視圖控制器一樣,它管理一個根視圖和一些內容。這兩者之間的不同點是,容器視圖控制器從其它視圖控制器那裡得到一部分內容,得到的內容局限於其它視圖控制器的視圖,這些內容內嵌於容器視圖控制器自己的視圖層次中。容器視圖控制器為任何內嵌的視圖設置大小和位置,但是原先的視圖控制器仍然管理這些視圖中的內容。
當設計你自己的容器視圖控制器時,應該始終理解container 和 contained 容器視圖控制器之間的關系。視圖控制器之間的關系可以幫助通知它們的內容應該怎樣顯示到屏幕上,你的容器應該怎樣在內部管理它們。在設計的過程中,應該首先問問你自己如下這些問題:
-你的容器和容器的children 扮演什麼樣的角色?
-多少children可以被同時顯示?
-兄弟視圖控制器之間的關系是怎樣的?
-如何在容器中添加或者移除子視圖控制器?
-children 的大小和位置可以改變嗎?什麼樣的條件下這樣的改變會發生?
-容器本身提供decorative 或者navigation-related 視圖嗎?
-在容器和它的children 之間什麼樣的交流是是應該提供的?容器除了類UIViewController 中定義的標准事件外,還需要向它的children report指定的事件嗎?
-可以用不同的方式類配置容器的顯示嗎?如果可以,應該怎樣配置?
在你定義了各種不同的對象之間的規則後,實現一個容器視圖控制器是相當簡單的。UIKit 的唯一要求是你需要在容器視圖控制器和它的任一子視圖控制器之間建立一個正式的parent-child關系。parent-child關系可以確保children 接收到系統發來的相關信息。除此之外,大多數實際的工作發生在布局和管理它所包含的視圖期間,每個容器視圖控制器在這點上都是不同的。你可以在容器視圖控制器內容區域的任何地方放置視圖,也可以在該區域內設置視圖為任何你想要的大小。你也可以在導航中添加定制的視圖到視圖層次中,用來提供裝飾或者幫助。
Example:Navigation Controller
一個UINavigationController 對象通過一個分層次的數據設置來支持導航。一個導航界面一次呈現一個子視圖控制器。界面頂部的導航條展示了在數據層次中的當前位置,還展示了一個後退按鈕用來退回到上一層。導航下降到數據層是由子視圖控制器決定的,而且可能涉及到tables和buttons的使用。
視圖控制器之間的導航是被導航控制器和它的children 共同來管理的。當用戶通過一個子視圖控制器的按鈕或者table 行來進行交互的時候,子視圖控制器要求導航控制器推送一個新的視圖控制器到視圖,子視圖控制器負責處理新的視圖控制器的內容的配置,導航視圖控制器負責管理animations 過渡。導航控制器也負責管理導航條,它顯示了一個後退按鈕,用來dismiss 最上層的視圖控制器。
圖5-1展示了一個導航控制器和它的視圖之間的結構。絕大多數的內容區域是被最上層的子視圖控制器填充的,導航條只占據了很少的一部分。
圖5-1導航界面的結構
在compact 和 regular 環境中,導航視圖控制器一次僅顯示一個子視圖控制器。導航視圖控制器負責重新調整它的子視圖大小來適應有效的屏幕空間。
Example:Split View Controller
UISplitViewController 對象在一個主從排列中展示兩個視圖控制器的內容,在這個排列中,一個視圖控制器(主)決定其它的視圖控制器顯示到界面上的詳細內容。兩個視圖控制器的可見性都是可以配置的,但是它們都被當前的環境來支配。在一個水平的regular 環境中,split視圖控制器可以邊挨邊的顯示兩個視圖控制器,或者也可以隱藏主視圖,在需要的時候再顯示它。在一個compact環境中,split視圖控制器一次僅顯示一個視圖控制器。
圖5-2展示了在水平regular 環境中,一個split視圖控制器和它的視圖視圖之間的結構。split視圖控制器本身默認有它自己的容器視圖。在這個例子中,兩個視圖邊挨邊的顯示,子視圖的大小是可以配置的,主視圖在圖中也是可見的。
圖5-2一個split視圖界面
在Interface Builder中配置一個視圖
為了在設計時創建一個parent-child 容器關系,添加一個容器視圖對象到你的storyboard 屏幕上,如圖5-3那樣。一個容器視圖對象是一個占位符對象,它代表了子視圖控制器的內容。在容器中使用這個視圖來設置根視圖和其它有關視圖的大小和位置。
圖5-3在Interface Builder中添加一個容器視圖
當你為一個或者多個容器視圖加載一個視圖控制器的時候,Interface Builder也會加載和這個視圖相關的子視圖控制器。子視圖和父視圖必須被同時初始化,這樣一個合適的parent-child關系就可以被建立起來了。
如果你不適用Interface Builder來建立你的parent-child容器關系,你必須用編碼的方式通過把每個child添加到容器視圖控制器中的方式來創建這種關系,詳細描述,inAdding a Child View Controller to Your Content.
實現一個定制的容器視圖控制器
為了實現一個容器視圖控制器,你必須在視圖控制器和它的子視圖控制器之間建立關系。在你試著管理視圖和它任何子視圖控制器之前,建立這種parent-child關系是必須的。這樣做的話,可以讓UIKit知道你的視圖控制器正在管理它的子視圖的大小和位置。你可以通過Interface Builder 或者編碼的方式來創建這種關系。當你通過編碼的方式來創建這種關系時,作為建立視圖控制器的一部分,你應該顯示的添加和移除子視圖控制器。
在你的內容中添加一個子視圖控制器
為了以編碼的方式包含一個子視圖控制器到你的內容中,在相關的視圖控制器之間創建一個parent-child關系,可以通過下面這些步驟:
1.調用容器視圖控制器的addChildViewController:方法。 這個方法告訴UIKit 你的容器視圖控制器正在管理相關的視圖的子視圖控制器。
2.添加child 的根視圖到容器的視圖層次結構。 在這個過程中,應該始終記得設置child‘s frame 的大小和位置。
3.添加若干約束來管理child 根視圖的大小和位置。
4.調用子視圖控制器的didMoveToParentViewController:方法。
Listing5-1展示了一個容器時怎樣嵌入一個子視圖控制器到它的容器中的。在建立了parent-child關系後,容器設置它的child 的frame ,添加child 的視圖到它的視圖層次結構中。設置child 的視圖frame 大小時非常重要的,可以確保視圖在你的容器中正確的顯示。在添加了視圖之後,容器調用child的 didMoveToParentViewController:方法來給子視圖控制器一個響應視圖所有權改變的機會。(注意,這裡的child 指的是容器視圖控制器中的子視圖控制器)
Listing5-1 添加一個子視圖控制器到容器
- (void) displayContentController: (UIViewController*) content {
[self addChildViewController:content]; content.view.frame = [self frameForContentController]; [self.view addSubview:self.currentClientView]; [content didMoveToParentViewController:self]; }在上面的這個例子中,注意你僅僅調用了child的didMoveToParentViewController:方法。那是因為addChildViewController:方法為你調用了child 的willMoveToParentViewController:方法。你必須自己調用didMoveToParentViewController:方法的原因是在直到你嵌入child的視圖到容器的視圖層次結構中之後,這個方法不能被調用。
當使用自動布局時,在添加子視圖到你的容器的視圖層次結構中之後,在容器和它的child之間建立約束。你的約束應該僅僅影響child 根視圖的大小和位置,而不能改變child 視圖層次結構中根視圖或者是其它任何視圖的內容。
移除一個子視圖控制器
為了從你的內容中移除一個子視圖控制器,通過下面這些步驟來移除視圖控制器之間的parent-child關系:
1.調用child 值為nil 的willMoveToParentViewController:方法。
2.移除你在child 的根視圖上設置的任何約束。
3.從你的容器的視圖層次中移除child 的根視圖。
4.在parent-child關系的最後,調用child 的removeFromParentViewController 方法來結束。
移除一個子視圖控制器就永久的割斷了child 和parent 之間的聯系。僅僅當你不在需要引用一個子視圖控制器的時候才移除它。例如,當一個新的視圖控制器被push 到navigation stack時,導航視圖控制器不會移除它當前的子視圖控制器。僅僅當子視圖控制器被pop 出navigation stack 時才移除它。
Listing 5-2展示了如何移從容器中移除它的子視圖控制器。調用參數為nil 的willMoveToParentViewController:方法,給子視圖控制器一個為改變做好准備的機會的機會。removeFromParentViewController 方法也調用子視圖控制器的didMoveToParentViewController:方法,傳遞給該方法一個值為nil 的參數。最後設置父視圖控制器指向nil 從你的容器中移除子視圖控制器的視圖。
Listing5-2 從容器中移除一個子視圖控制器
- (void) hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil]; [content.view removeFromSuperview]; [content removeFromParentViewController]; }
在子視圖控制器之間切換
當你想要animate 一個子視圖控制器來替換另一個的時候,包含一個新添加的視圖控制器到transition animation 過程的同時移除另一個子視圖控制器。在animations 之前,確保兩個子視圖控制器都是容器內容的一部分,並且讓當前的子視圖控制器知道自己即將離開。在animations 期間,移動新的子視圖控制器的視圖到相應的位置同時移除先前的子視圖控制器的視圖。在animation 完成後,完成了子視圖控制器的移除。
Listing5-3 展示了怎樣使用transition animation 來交換兩個子視圖控制器。在這個例子中,新的視圖控制器被animate 到被當前已存在的子視圖控制器占據的矩形區域,該子視圖控制器被移除屏幕外。在animations 完成之後,completion block 從容器中移除子視圖控制器。在這個例子中,transitionFromViewController:toViewController:duration:options:animations:completion: 方法自動地更新容器的視圖層次,所以不需要你自己來手動的添加和移除這些視圖。
Listing5-3 在兩個子視圖控制器之間切換
為Children 管理appearance 更新
在添加一個子視圖控制器到容器後,容器會自動發送appearance-related 信息給該子視圖控制器。這是你想要的正常行為,因為它可以確保所有的事件都被正常的發送。然而,有時候默認的行為可能以一種對容器來說不合理的順序來發送這些事件。例如,如果多個children 同時改變他們的視圖狀態,你可能想要合並這些改變,讓appearance callbacks 以一種更合理的順序同時發生。
為了接管appearance callbacks 的責任,在你的容器視圖控制器中重載 shouldAutomaticallyForwardAppearanceMethods
方法,並且返回NO,如Listing5-4那樣。返回NO,讓UIKit知道你的容器視圖控制器用自己的appearance 來通知它的children這種改變。
Listing5-4Disabling automatic appearance forwarding
- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
return NO; }
beginAppearanceTransition:animated:
方法或者endAppearanceTransition方法。例如,如果你的容器中有一個單一的視圖控制器引用,通過一個child屬性。你的容器應該發送下面Listing5-5中的信息給子視圖控制器。
Listing 5-5Forwarding appearance messages when the container appears or disappears
-(void) viewWillAppear:(BOOL)animated {
[self.child beginAppearanceTransition: YES animated: animated]; } -(void) viewDidAppear:(BOOL)animated { [self.child endAppearanceTransition]; } -(void) viewWillDisappear:(BOOL)animated { [self.child beginAppearanceTransition: NO animated: animated]; } -(void) viewDidDisappear:(BOOL)animated { [self.child endAppearanceTransition]; }
構建一個容器視圖控制器的建議
花點時間好好的設計,開發和測試一個新的容器視圖控制器。盡管容器的個別行為是直截了當的,但是容器作為一個整體是相當復雜的。當你實現你自己的容器類時考慮下下面這些建議:
- 僅僅訪問一個子視圖控制器的根視圖。 容器應該僅僅訪問每個子視圖控制器的根視圖,也就是,被子視圖控制器的view
屬性返回的視圖。容易應該從不訪問子視圖控制器的其它視圖。
- 子視圖控制器應該最小限度的了解它們的容器。 子視圖控制器應該聚焦在自己的內容中,如果容器允許它的行為可以受另一個子視圖控制器的影響,那麼它應該使用delegation 設計模式來管理這些交互。
- 首先使用regular 視圖來設計你的容器。使用regular 視圖(而不是來自子視圖控制器的視圖)給你一個在簡單的環境中測試布局約束和animated transitions 的機會。當regular 視圖如期望中的那樣工作時,在你的子視圖控制器中尋找一個合適的視圖來交換它。
子視圖控制器的委托控制
一個容器視圖控制器可以delegate 它自己的appearance 的一些方面給一個或者多個它的子視圖控制器。你可以用下面這些方法delegate control:
- 讓一個子視圖控制器決定狀態條的風格。 為了delegate 狀態條appearance 給一個子視圖控制器,可以在你的容器視圖控制器中重載一個或者兩個childViewControllerForStatusBarStyle
andchildViewControllerForStatusBarHidden
方法。
- 讓子視圖控制器指定自己的preferred size。 一個有靈活布局的容器可以使用子視圖控制器自己的preferredContentSize 屬性來幫助決定子視圖的size。