objc系列譯文(1.4):View Controller 容器

發表於2014-05-30

在 iOS 5 之前,view controller 容器是 Apple 的特權。實際上,在 view controller 程式設計指南中還有一段申明,指出你不應該使用它們。Apple 對 view controllers 的總的建議曾經是“一個 view controller 管理一個全螢幕的內容”。這個建議後來被改為“一個 view controller 管理一個自包含的內容單元”。為什麼 Apple 不想讓我們構建自己的 tab bar controllers 和 navigation controllers?或者更確切地說,這段程式碼有什麼問題:

view-insertion

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 程式碼庫並執行這個程式,它有助於讓你更容易理解示例程式碼。(劇透:從丹麥西部開始,挖穿地球,你會到達南太平洋的某個地方)

tunnel-screenshot

為了尋找對跖點,也稱作相反的座標,將拿著鏟子的小孩四處移動,地圖會告訴你對應的出口位置在哪裡。點選雷達按鈕,地圖會翻轉過來顯示位置的名稱。

螢幕上有兩個 map view controllers。每個都需要控制地圖的拖動,標註和更新。翻過來會顯示兩個新的 view controllers,用來檢索地理位置。所有的 view controllers 都包含於一個 parent view controller 中,它持有它們的 views,並保證正確的佈局和旋轉行為。

Root view controller 有兩個 container views。新增它們是為了讓佈局,以及 child view controllers 的 views 的動畫做起來更容易,我們馬上就可以看到。

我們例項化了 _startMapViewController,用來顯示起始位置,並設定了用於標註的影象。

  1. _startMapViewcontroller 被新增成 root view controller 的一個 child。這會自動在 child 上呼叫willMoveToParentViewController: 方法。
  2. child 的 view 被新增成 container view 的 subview。
  3. child 被通知到它現在有一個 parent view controller。
  4. 用來顯示地理位置的 child view controller 被例項化了,但是還沒有被插入到任何 view 或 controller 層級中。

佈局

Root view controller 定義了兩個 container views,它決定了 child view controller 的大小。Child view controllers 不知道會被新增到哪個容器中,因此必須適應大小。

現在,它們就會用 super view 的 bounds 來進行佈局。這樣增加了 child view controller 的可複用性;如果我們把它 push 到 navigation controller 的棧中,它仍然會正確地佈局。

過場動畫

Apple 已經針對 view controller 容器做了細緻的 API,我們可以構造我們能想到的任何容器場景的動畫。Apple 還提供了一個基於 block 的便利方法,來切換螢幕上的兩個 controller views。方法 transitionFromViewController:toViewController:(...)已經為我們考慮了很多細節。

  1. 在開始動畫之前,我們把 toController 作為一個 child 進行新增,並通知 fromController 它將被移除。如果fromController 的 view 是容器 view 層級的一部分,它的 viewWillDisapear: 方法就會被呼叫。
  2. toController 被告知它有一個新的 parent,並且適當的 view 事件方法將被呼叫。
  3. 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 的屬性。

當這個屬性跟著拿著鏟子的小孩的移動而改變時,parent view controller 將新座標的對跖點傳遞給另一個地圖:

類似地,當你點選雷達按鈕,parent view controller 給新的 child view controllers 設定待檢索的座標。

我們想要達到的目標和你選擇的手段無關,從 child 到 parent view controller 訊息傳遞的技術,不論是採用 KVO,通知,或者是委託模式,child view controller 都應該獨立和可複用。在我們的例子中,我們可以將某個 child view controller 推入到一個 navigation 棧中,它仍然能夠通過相同的 API 進行通訊。

 

相關文章