翻譯自“View Controller Programming Guide for iOS”。
1 視圖控制器的角色
視圖控制器是應用程序內部結構的基礎。每個應用至少有一個視圖控制器,大多是應用程序有多個。每個視圖控制器管理應用程序的部分用戶界面,以及界面和底層數據之間的交互。視圖控制器也便于用戶界面不同部分之間的過渡。
因為視圖控制器在應用程序中扮演了如此重要的角色,所以它們幾乎是你做一切事情的中心。UIViewController類定義了管理視圖,處理事件,從一個視圖控制器過渡到另一個,與應用程序其它部分交互的方法和屬性。可以從UIViewController(或它的子類)繼承,并添加自定義代碼來實現應用程序的行為。
有兩種類型的視圖控制器:
- 內容視圖控制器管理獨立的應用程序內容,你創建的大部分視圖控制器都是這種類型。
- 容器視圖控制器從其它視圖控制器(稱為子視圖控制器)收集信息,并用一種不同的,方便導航或顯示這些視圖控制器內容的方式顯示。
大部分應用程序混用兩種類型的視圖控制器。
1.1 視圖管理
視圖控制器最重要的作用是管理視圖的層級結構。每個視圖控制器都有唯一的根視圖,該視圖包含所有視圖控制器的內容。添加需要顯示的內容到根視圖。圖1-1描述了視圖控制器和它的視圖之間的內在關系。視圖控制器總有一個指向根視圖的引用,每個視圖有一個指向它的子視圖的強引用。
圖1-1 視圖控制器和它的視圖之間的關系

提示:常見的做法是使用outlet訪問視圖控制器中視圖層級結構的其它視圖。因為視圖控制器管理它所有子視圖的內容,所以outlet可以存儲指向需要的視圖的引用。當從故事版中加載視圖時,outlet本身自動連接到實際的視圖對象。
內容視圖控制器管理自身的所有視圖。容器視圖控制器除了管理自身的視圖,還要管理一個或多個子視圖控制器的根視圖。容器不管理子視圖控制器的內容。它只管理根視圖,并根據容器的設計管理根視圖的尺寸和位置。圖1-2描述了分割視圖控制器和它的子視圖控制器之間的關系。分割視圖控制器管理子視圖的整體尺寸和位置,而子視圖控制器管理這些視圖的實際內容。
圖1-2 視圖控制器可以管理其它視圖控制器的內容

如何管理視圖控制器的視圖,請參考“管理視圖布局”。
1.2 數據打包
視圖控制器是它管理的視圖和應用程序數據之間的媒介。UIViewController類的方法和屬性可以用來管理應用程序的視覺呈現。繼承UIViewController類時,可以在子類中添加任意需要的變量來管理數據。添加自定義變量會創建類似圖1-3中的關系,視圖控制器有指向數據的引用和用來顯示數據的視圖的引用。你負責兩者之間的數據通信。
圖1-3 視圖控制器是數據對象和視圖之間的媒介

你應該始終保持視圖控制器和數據對象之間職責分離。確保數據結構完整性的大部分邏輯屬于數據對象本身。視圖控制器可能會驗證來自視圖的輸入,然后以數據對象要求的格式打包該輸入,但應該最小化視圖控制器在管理實際數據上的工作。
UIDocument對象是管理數據的一種方式,該方式可以保持視圖控制器和數據對象之間職責分離。文檔對象是知道如何讀寫數據到持久存儲的控制器對象。繼承UIDocument時,可以添加任何邏輯和方法來提取數據,并把它傳遞給視圖控制器或應用程序其它部分。視圖控制器可能會存儲它收到的任何數據的副本,以便更新視圖,但是文檔對象仍然擁有真實的數據。
1.3 用戶交互
視圖控制器是響應者對象(responder object),能夠處理響應鏈中的事件。盡管視圖控制器可以這么做,但它們很少直接處理觸摸事件。相反,通常視圖處理自己的觸摸事件,并把結果反饋給關聯的代理或目標對象(通常是視圖控制器)的方法。所以,使用代理方法或動作方法(action method)處理視圖控制器中的大部分事件。
關于如何在視圖控制器中實現動作方法,請參考“處理用戶交互”。關于處理其它類型的事件,請參考“iOS事件處理指南”。
1.4 資源管理
視圖控制器負責它的視圖和它創建的任何對象。UIViewController類自動處理視圖管理的大部分方面。例如,UIKit自動釋放不要需要的視圖相關資源。在UIViewController子類中,你負責管理顯式創建的任何對象。
當可用內存不足時,UIKit要求應用釋放所有不再需要的資源。其中一種方式是調用視圖控制器的didReceiveMemoryWarning方法。使用該方法移除指向不再需要的對象的引用,或者稍后可以很容易重新創建的引用。例如,使用該方法移除緩存數據。當內存不足時,盡可能多的釋放內存。消耗太多內存的應用可能會被系統完全終止,以便恢復內存。
1.5 自適應
視圖控制器負責顯示視圖,和調整顯示來匹配底層環境。每個iOS應用應該可以在iPad和不同尺寸的iPhone上運行。比起為每種設備提供不同的視圖控制器和視圖層級結構,使用單個視圖控制器,并讓它的視圖適應空間的變化更簡單。
在iOS中,視圖控制器需要處理粗粒度和細粒度的變化。粗粒度變化發生在視圖控制器的特征(trait)改變時。特征是描述整體環境的屬性,比如顯示比例。兩個最重要的特征是視圖控制器水平和垂直方向的尺寸類(size class),表示在給定的尺寸中,視圖控制器有多少可用空間。可以使用尺寸類的變化來改變視圖布局的方式,如圖1-4所示。當水平方向的尺寸類是常規時,視圖控制器利用額外的水平空間排列內容。當水平方向的尺寸類是緊湊時,視圖控制器垂直排列內容。
圖1-4 讓視圖適應尺寸類的變化

給定尺寸類后,在任何時候都有可能發生更細粒度的尺寸變化。當用戶將iPhone從豎屏旋轉到橫屏時,尺寸類也許不會變化,但屏幕尺寸通常會變化。使用自動布局(Auto Layout)時,UIKit自動調整視圖的尺寸和位置來匹配新的尺寸。視圖控制器可以根據需要做額外的調整。
更多關于自適應的信息請參考“自適應模型”。
2 視圖控制器層級結構
應用程序的視圖控制器之間的關系定義了每個視圖控制器所需要的行為。UIKit希望你按規定的方式使用視圖控制器。保持適當的視圖控制器關系可以保證,需要時那些自動行為可以傳遞給正確的視圖控制器。如果違反了規定的包含和顯示關系,應用程序的一部分不會按預期運行。
2.1 根視圖控制器
根視圖控制器是視圖控制器層級結構中的支撐點。每個窗口都恰好有一個根視圖控制器,其內容充滿該窗口。根視圖口控制器定義了用戶看到的初始內容。圖2-1展示了根視圖控制器和窗口之間的關系。因為窗口本身沒有可見內容,所以視圖控制器的視圖提供了所有內容。
圖2-1 根視圖控制器

根視圖控制器通過UIWindow對象的rootViewController屬性訪問。使用故事版配置視圖控制器時,UIKit在啟動時自動設置該屬性的值。對于通過代碼創建的窗口,必須自己設置根視圖控制器。
2.2 容器視圖控制器
容器視圖控制器可以把更好管理和重用的部件組合為復雜的界面。容器視圖控制器混合一個或多個帶有可選自定義視圖的子視圖控制器來創建最終的界面。例如,UINavigationController對象顯示帶有導航欄和可選工具欄的子視圖控制器的內容,導航欄和可選的工具欄由導航控制器管理。UIKit包含幾個容器視圖控制器:UINavigationController,UISplitViewController和UIPageViewController。
容器視圖控制器的視圖總是充滿給它的空間。容器視圖控制器通常作為窗口的根視圖控制器(如圖2-2所示),但它們也可以模態的顯示或者作為其它容器的子視圖控制器。容器負責適當的定位它們的子視圖。在圖中,容器并排放置兩個子視圖。盡管子視圖控制器依賴于容器界面,但它們幾乎不知道容器和任何兄弟視圖控制器。
圖2-2 容器視圖控制器作為根視圖控制

因為容器視圖控制器管理它的子視圖控制器,所以UIKit定義了如何在自定義容器中子視圖控制器的規則。如何創建自定義容器視圖控制器的詳細信息,請參考“實現容器視圖控制器”。
2.3 Presented視圖控制器
顯示一個視圖控制器會用新視圖控制器的內容代替當前視圖控制器的內容,通常是隱藏前一個視圖控制器的內容。彈出(presentation)最常用來模態顯示新的內容。例如,顯示一個視圖控制器用來收集用戶的輸入。也可以把它們作為應用程序界面的通用組成部分。
顯示一個視圖控制器時,UIKit在presenting視圖控制器和presented視圖控制器之間創建了如圖2-3所示的關系。(也存在從presented視圖控制器到presenting控制器的反向關系。)這些關系是視圖控制器層級結構的一部分,同時也是運行時定位其它視圖控制器的一種方式。
圖2-3 presented視圖控制器

涉及到容器視圖控制器時,UIKit可能會修改顯示鏈來簡化必須編寫的代碼。不同的顯示風格如何在屏幕上顯示有不同的規則。例如,全屏顯示總是覆蓋整個屏幕。顯示視圖控制器時,UIKit尋找一個為顯示提供合適上下文的視圖控制器。很多情況下,UIKit選擇最近的容器視圖控制器,但也可能選擇窗口的根視圖控制器。某些情況下,也可以告知UIKit哪個視圖控制器定義了顯示上下文,應該處理該顯示。
圖2-4展示了為什么通常容器提供了顯示上下文。執行全屏顯示時,新的視圖控制器需要覆蓋整個屏幕。容器決定是否處理顯示,而不是要求子視圖控制器知道容器的bounds。因為例子中的導航控制器覆蓋了整個屏幕,所以它作為presenting視圖控制器,并初始化顯示。
圖2-4 容器和presented視圖控制器

3 設計技巧
視圖控制器是iOS上應用程序運行的重要工具,UIKit中的視圖控制器基礎架構可以不用編寫大量的代碼就能創建復雜的界面。實現自己的視圖控制器時,使用下面的技巧和指南來確保不會干擾系統期望的行為。
3.1 盡可能使用系統提供的視圖控制器
很多iOS框架定義了可以使用的視圖控制器。使用系統提供的視圖控制器可以節省時間,并確保用戶體檢的一致性。
絕大部分系統視圖控制器是為特定任務設計的。一些視圖控制器可以訪問用戶數據,例如聯系人。其它視圖控制器可以訪問硬件,或者為管理多媒體提供特殊的調解界面。例如,UIKit中的UIImagePickerController類顯示一個標準界面,用來捕獲圖片和視圖,以及訪問用戶的相冊。
創建自定義視圖控制器之前,查看現在框架中是否存在滿足需求的視圖控制器。
- UIKit框架提供了顯示警告框,拍攝圖片和視頻,管理iCloud文件的視圖控制器。還定義了很多用來組織內容的標準容器視圖控制器。
- GameKit框架提供了匹配玩家,管理排行榜,成就和其它游戲特性的視圖控制器。
- Address Book UI框架提供了顯示和選擇聯系人信息的視圖控制器。
- MediaPlayer框架提供了顯示和管理視圖,以及從用戶庫中選擇多媒體資源的視圖控制器。
- EventKit UI框架提供了顯示和編輯用戶日歷數據的視圖控制器。
- GLKit框架提供了管理OpenGL渲染表面的視圖控制器。
- Multipeer Connectivity框架提供了檢測其它用戶和邀請連接的視圖控制器。
- Message UI框架提供了編寫電子郵件和短信的視圖控制器。
- PassKit框架提供了顯示pass和添加pass到Passbook的視圖控制器。
- Social框架提供了編寫Twitter,Facebook和其它社交媒體網站消息的視圖控制器。
- AVFoundation框架提供了顯示多媒體資源的視圖控制器。
重要:永遠不要修改系統提供的視圖控制器的視圖層級結構。每個視圖控制器有自己的視圖層級結構,并負責維護該層級結構的完整性。改變層級結構可能會在代碼中引入bug,或者讓其中的視圖控制器不能正常工作。在系統視圖控制器中,總是使用公有方法和屬性修改視圖控制器。
關于使用特定視圖控制器的信息,請參考相應的框架文檔。
3.2 讓每個視圖控制器相互獨立
視圖控制器應該總是自我獨立的對象。視圖控制器不應該了解內部運作,或者其它視圖控制器的視圖層級結構。兩個視圖控制器需要通信或者相互傳遞數據時,應該使用顯式定義的公開接口。
代理設計模式經常用來管理視圖控制器之間的通信。使用代理時,一個對象定義一個協議,用來與關聯的代理對象通信,該代理對象遵循這個協議。代理對象的確切類型不重要。重要的是該對象實現了協議的方法。
3.3 根視圖只作為其它視圖的容器
視圖控制器的根視圖僅僅作為內容的容器。使用根視圖作為容器讓所有視圖有公有的父視圖,可以讓許多布局操作更簡單。很多自動布局約束要求有公有的父視圖來正確的布局視圖。
3.4 知道你的數據在哪
在model-view-controller設計模式中,視圖控制器的作用是方便數據在模型對象和視圖對象之間傳遞。視圖控制器可能在臨時變量中存儲一些數據,并執行一些驗證,但它的主要職責是確保它的視圖包括準確的信息。數據對象負責管理真正的數據,并確保屬性的完整性。
UIDocument和UIViewController類之間的關系就是分割數據和界面的例子。具體的說,兩者之間不存在默認關系。UIDocument對象協調加載和保存數據,而UIViewController對象協調在屏幕上顯示視圖。如果在兩個對象之間創建了關系,記住視圖控制器從文檔中緩存信息只是為了效率。真正的數據仍然屬于文檔對象。
3.5 適應變化
應用程序可以在各種iOS設備上運行,視圖控制器設計為可以適應這些設備的不同尺寸屏幕。使用內置的自適應支持來響應視圖控制器中尺寸和尺寸類的變化,而不是使用不同的視圖控制器來管理不同屏幕上的內容。UIKit發送的通知讓你可以大規模和小規模改變用戶界面,而不用改變視圖控制器的其余代碼。
關于處理自適應變化的更多信息,請參考“自適應模型”。