鍍金池/ 教程/ iOS/ View Controller 容器
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫(xiě)腳本
結(jié)構(gòu)體和值類(lèi)型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開(kāi)發(fā)
Collection View 動(dòng)畫(huà)
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫(huà)
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類(lèi)
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫(huà)解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫(huà)
常見(jiàn)的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類(lèi)
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類(lèi)的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫(huà)
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺(jué)
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開(kāi)發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

View Controller 容器

在 iOS 5 之前,view controller 容器是 Apple 的特權(quán)。實(shí)際上,在 view controller 編程指南中還有一段申明,指出你不應(yīng)該使用它們。Apple 對(duì) view controllers 的總的建議曾經(jīng)是“一個(gè) view controller 管理一個(gè)全屏幕的內(nèi)容”。這個(gè)建議后來(lái)被改為“一個(gè) view controller 管理一個(gè)自包含的內(nèi)容單元”。為什么 Apple 不想讓我們構(gòu)建自己的 tab bar controllers 和 navigation controllers?或者更確切地說(shuō),這段代碼有什么問(wèn)題:

[viewControllerA.view addSubView:viewControllerB.view]

http://wiki.jikexueyuan.com/project/objc/images/1-2.png" alt="Inconsistent view hierarchy" />

UIWindow 作為一個(gè)應(yīng)用程序的根視圖(root view),是旋轉(zhuǎn)和初始布局消息等事件產(chǎn)生的來(lái)源。在上圖中,child view controller 的 view 插入到 root view controller 的視圖層級(jí)中,被排除在這些事件之外了。View 事件方法諸如 viewWillAppear: 將不會(huì)被調(diào)用。

在 iOS 5 之前構(gòu)建自定義的 view controller 容器時(shí),要保存一個(gè) child view controller 的引用,還要手動(dòng)在 parent view controller 中轉(zhuǎn)發(fā)所有 view 事件方法的調(diào)用,要做好非常困難。

一個(gè)例子

當(dāng)你還是個(gè)孩子,在沙灘上玩時(shí),你父母是否告訴過(guò)你,如果不停地用鏟子挖,最后會(huì)到達(dá)美國(guó)?我父母就說(shuō)過(guò),我就做了個(gè)叫做 Tunnel 的 demo 程序來(lái)驗(yàn)證這個(gè)說(shuō)法。你可以 clone 這個(gè) Github 代碼庫(kù)并運(yùn)行這個(gè)程序,它有助于讓你更容易理解示例代碼。(劇透:從丹麥西部開(kāi)始,挖穿地球,你會(huì)到達(dá)南太平洋的某個(gè)地方)

http://wiki.jikexueyuan.com/project/objc/images/1-3.png" alt="Tunnel screenshot" />

為了尋找對(duì)跖點(diǎn),也稱(chēng)作相反的坐標(biāo),將拿著鏟子的小孩四處移動(dòng),地圖會(huì)告訴你對(duì)應(yīng)的出口位置在哪里。點(diǎn)擊雷達(dá)按鈕,地圖會(huì)翻轉(zhuǎn)過(guò)來(lái)顯示位置的名稱(chēng)。

屏幕上有兩個(gè) map view controllers。每個(gè)都需要控制地圖的拖動(dòng),標(biāo)注和更新。翻過(guò)來(lái)會(huì)顯示兩個(gè)新的 view controllers,用來(lái)檢索地理位置。所有的 view controllers 都包含于一個(gè) parent view controller 中,它持有它們的 views,并保證正確的布局和旋轉(zhuǎn)行為。

Root view controller 有兩個(gè) container views。添加它們是為了讓布局,以及 child view controllers 的 views 的動(dòng)畫(huà)做起來(lái)更容易,我們馬上就可以看到。

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

我們實(shí)例化了 _startMapViewController,用來(lái)顯示起始位置,并設(shè)置了用于標(biāo)注的圖像。

  1. _startMapViewcontroller 被添加成 root view controller 的一個(gè) child。這會(huì)自動(dòng)在 child 上調(diào)用 willMoveToParentViewController: 方法。
  2. child 的 view 被添加成 container view 的 subview。
  3. child 被通知到它現(xiàn)在有一個(gè) parent view controller。
  4. 用來(lái)顯示地理位置的 child view controller 被實(shí)例化了,但是還沒(méi)有被插入到任何 view 或 controller 層級(jí)中。

布局

Root view controller 定義了兩個(gè) container views,它決定了 child view controller 的大小。Child view controllers 不知道會(huì)被添加到哪個(gè)容器中,因此必須適應(yīng)大小。

- (void) loadView
{
    mapView = [MKMapView new];
    mapView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [mapView setDelegate:self];
    [mapView setMapType:MKMapTypeHybrid];

    self.view = mapView;
}

現(xiàn)在,它們就會(huì)用 super view 的 bounds 來(lái)進(jìn)行布局。這樣增加了 child view controller 的可復(fù)用性;如果我們把它 push 到 navigation controller 的棧中,它仍然會(huì)正確地布局。

過(guò)場(chǎng)動(dòng)畫(huà)

Apple 已經(jīng)針對(duì) view controller 容器做了細(xì)致的 API,我們可以構(gòu)造我們能想到的任何容器場(chǎng)景的動(dòng)畫(huà)。Apple 還提供了一個(gè)基于 block 的便利方法,來(lái)切換屏幕上的兩個(gè) controller views。方法 transitionFromViewController:toViewController:(...) 已經(jīng)為我們考慮了很多細(xì)節(jié)。

- (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. 在開(kāi)始動(dòng)畫(huà)之前,我們把 toController 作為一個(gè) child 進(jìn)行添加,并通知 fromController 它將被移除。如果 fromController 的 view 是容器 view 層級(jí)的一部分,它的 viewWillDisapear: 方法就會(huì)被調(diào)用。
  2. toController 被告知它有一個(gè)新的 parent,并且適當(dāng)?shù)?view 事件方法將被調(diào)用。
  3. fromController 被移除了。

這個(gè)為 view controller 過(guò)場(chǎng)動(dòng)畫(huà)而準(zhǔn)備的便捷方法會(huì)自動(dòng)把老的 view controller 換成新的 view controller。然而,如果你想實(shí)現(xiàn)自己的過(guò)場(chǎng)動(dòng)畫(huà),并且希望一次只顯示一個(gè) view,你需要在老的 view 上調(diào)用 removeFromSuperview,并為新的 view 調(diào)用 addSubview:。錯(cuò)誤的調(diào)用次序通常會(huì)導(dǎo)致 UIViewControllerHierarchyInconsistency 警告。例如:在添加 view 之前調(diào)用 didMoveToParentViewController: 就觸發(fā)這個(gè)警告。

為了能使用 UIViewAnimationOptionTransitionFlipFromTop 動(dòng)畫(huà),我們必須把 children's view 添加到我們的 view containers 里面,而不是 root view controller 的 view。否則動(dòng)畫(huà)將導(dǎo)致整個(gè) root view 都翻轉(zhuǎn)。

通信

View controllers 應(yīng)該是可復(fù)用的、自包含的實(shí)體。Child view controllers 也不能違背這個(gè)經(jīng)驗(yàn)法則。為了達(dá)到目的,parent view controller 應(yīng)該只關(guān)心兩個(gè)任務(wù):布局 child view controller 的 root view,以及與 child view controller 暴露出來(lái)的 API 通信。它絕不應(yīng)該去直接修改 child view tree 或其他內(nèi)部狀態(tài)。

Child view controller 應(yīng)該包含管理它們自己的 view 樹(shù)的必要邏輯,而不是把它們看作單純呆板的 views。這樣,就有了更清晰的關(guān)注點(diǎn)分離和更好的可復(fù)用性。

在示例程序 Tunnel 中,parent view controller 觀察了 map view controllers 上的一個(gè)叫 currentLocation 的屬性。

[_startMapViewController addObserver:self
                          forKeyPath:@"currentLocation"
                             options:NSKeyValueObservingOptionNew
                             context:NULL];

當(dāng)這個(gè)屬性跟著拿著鏟子的小孩的移動(dòng)而改變時(shí),parent view controller 將新坐標(biāo)的對(duì)跖點(diǎn)傳遞給另一個(gè)地圖:

[oppositeController updateAnnotationLocation:[newLocation antipode]];

類(lèi)似地,當(dāng)你點(diǎn)擊雷達(dá)按鈕,parent view controller 給新的 child view controllers 設(shè)置待檢索的坐標(biāo)。

[_startGeoViewController setLocation:_startMapViewController.currentLocation];
[_targetGeoViewController setLocation:_targetMapViewController.currentLocation];

我們想要達(dá)到的目標(biāo)和你選擇的手段無(wú)關(guān),從 child 到 parent view controller 消息傳遞的技術(shù),不論是采用 KVO,通知,或者是委托模式,child view controller 都應(yīng)該獨(dú)立和可復(fù)用。在我們的例子中,我們可以將某個(gè) child view controller 推入到一個(gè) navigation 棧中,它仍然能夠通過(guò)相同的 API 進(jìn)行通信。