View Controller 容器

在 iOS 5 以前,view controller 容器是 Apple 的特權。實際上,在 view controller 編程指南中還有一段申明,指出你不該該使用它們。Apple 對 view controllers 的總的建議曾經是「一個 view controller 管理一個全屏幕的內容」。這個建議後來被改成「一個 view controller 管理一個自包含的內容單元」。爲何 Apple 不想讓咱們構建本身的 tab bar controllers 和 navigation controllers?或者更確切地說,這段代碼有什麼問題:html

[viewControllerA.view addSubView:viewControllerB.view]
複製代碼

UIWindow 做爲一個應用程序的根視圖(root view),是旋轉和初始佈局消息等事件產生的來源。在上圖中,child view controller 的 view 插入到 root view controller 的視圖層級中,被排除在這些事件以外了。View 事件方法諸如 viewWillAppear: 將不會被調用。git

在 iOS 5 以前構建自定義的 view controller 容器時,要保存一個 child view controller 的引用,還要手動在 parent view controller 中轉發全部 view 事件方法的調用,要作好很是困難。github

一個例子

當你仍是個孩子,在沙灘上玩時,你父母是否告訴過你,若是不停地用鏟子挖,最後會到達美國?我父母就說過,我就作了個叫作 Tunnel 的 demo 程序來驗證這個說法。你能夠 clone 這個 Github 代碼庫並運行這個程序,它有助於讓你更容易理解示例代碼。(劇透:從丹麥西部開始,挖穿地球,你會到達南太平洋的某個地方)編程

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

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

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,用來顯示起始位置,並設置了用於標註的圖像。動畫

  1. _startMapViewcontroller 被添加成 root view controller 的一個 child。這會自動在 child 上調用 willMoveToParentViewController: 方法。ui

  2. child 的 view 被添加成 container view 的 subview。spa

  3. child 被通知到它如今有一個 parent view controller。

  4. 用來顯示地理位置的 child view controller 被實例化了,可是尚未被插入到任何 view 或 controller 層級中。

    ###佈局

    Root view controller 定義了兩個 container views,它決定了 child view controller 的大小。Child view controllers 不知道會被添加到哪一個容器中,所以必須適應大小。

{
    mapView = [MKMapView new];
    mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [mapView setDelegate:self];
    [mapView setMapType:MKMapTypeHybrid];

    self.view = mapView;
}
複製代碼

如今,它們就會用 super view 的 bounds 來進行佈局。這樣增長了 child view controller 的可複用性;若是咱們把它 push 到 navigation controller 的棧中,它仍然會正確地佈局。

過場動畫

Apple 已經針對 view controller 容器作了細緻的 API,咱們能夠構造咱們能想到的任何容器場景的動畫。Apple 還提供了一個基於 block 的便利方法,來切換屏幕上的兩個 controller views。方法 transitionFromViewController:toViewController:(...) 已經爲咱們考慮了不少細節。

- (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
                            }];
}

複製代碼
  1. 在開始動畫以前,咱們把 toController做爲一個 child
  2. 進行添加,並通知fromController它將被移除。若是fromController 的 view 是容器 view 層級的一部分,它的viewWillDisappear: 方法就會被調用。
  3. toController 被告知它有一個新的 parent,而且適當的 view 事件方法將被調用。 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 的屬性。

[_startMapViewController addObserver:self
                          forKeyPath:@"currentLocation"
                             options:NSKeyValueObservingOptionNew
                             context:NULL];
複製代碼

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

[oppositeController updateAnnotationLocation:[newLocation antipode]];
複製代碼

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

[_startGeoViewController setLocation:_startMapViewController.currentLocation];
[_targetGeoViewController setLocation:_targetMapViewController.currentLocation];
複製代碼

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

原文地址 Testing View Controllers

相關文章
相關標籤/搜索