View Controller 容器

轉自:http://tang3w.com/translate/objective-c/objc.io/2013/10/28/view-controller-容器.html


注:這篇翻譯已經過 objc.io 授權,原文鏈接是:View Controller Containment

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

[viewControllerA.view addSubView:viewControllerB.view]

Inconsistent view hierarchy

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 的動畫做起來更容易,我們馬上就可以看到。

- (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: 方法。
  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 不知道會被添加到哪個容器中,因此必須適應大小。

- (void) loadView
{
    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 進行添加,並通知 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 的屬性。

[_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 進行消息傳遞。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章