iOS視圖控制器編程指南-Part 2:視圖控制器的定義

翻譯自“View Controller Programming Guide for iOS”。

1 定義子類

使用UIViewController的自定義子類來顯示應用程序內容。大部分自定義視圖控制器是內容視圖控制器,也就是說它們擁有自己的所有視圖,并負責管理這些視圖的數據。相比之下,容器視圖并不擁有所有視圖;有些視圖由其它視圖控制器管理。定義內容和容器視圖控制器的大部分步驟都是一樣的,這些將在下面的章節中討論。

內容視圖控制器最常見的父類如下:

  • 使用UITableViewController,尤其當視圖控制器的主視圖是表格時。
  • 使用UICollectionViewController,尤其當視圖控制器的主視圖是集合視圖時。
  • 其它所有視圖控制器使用UIViewController。

容器視圖控制器的父類取決于修改現有的容器類,還是創建自己的容器類。對于現有的容器,選擇想要修改的視圖控制器類。對于新的容器視圖控制器,通常從UIViewController繼承。關于創建容器視圖控制器的額外信息,請參考“實現容器視圖控制器”。

1.1 定義UI

在Xcode中使用故事版可視化定義視圖控制器的UI。雖然也可以通過代碼創建UI,但故事版是一個很好的方式來可視化視圖控制器的內容,并為不同的環境(如果需要)定制視圖層級結構。可視化構建UI可以快速的改變,并且不需要構建和運行應用程序就能看到結果。

圖4-1展示了故事版的例子。每一個矩形區域代表一個視圖控制器和它關聯的視圖。視圖控制器之間的箭頭是它們之間的關系和segue。關系連接容器視圖控制器到它的子視圖控制器。Segue可以在界面的視圖控制器之間導航。

圖4-1 包含一組視圖控制器和視圖的故事版

每個新項目都有一個主故事版,通常已經包括一個或多個視圖控制器。通過從庫中拖拽視圖控制器到畫布上,可以添加新的視圖控制器。新視圖控制器最初沒有關聯的類,所以必須使用Identity檢查器指定一個。

使用故事版編輯器完成以下操作:

  • 為視圖控制器添加,排列和配置視圖。
  • 連接outlet和action;參考“處理用戶交互”。
  • 在視圖控制器之間創建關系和segue;參考”Using Segues“。
  • 為不同的尺寸類(size classes)定制布局和視圖;參考”創建自適應界面“。
  • 添加手勢識別器處理視圖的用戶交互;參考”Event Handling Guide for iOS“。

1.2 處理用戶交互

應用程序的響應者對象處理接收的事件,并采取適當的動作。盡管視圖控制器是響應者對象,但它們幾乎不會直接處理觸摸事件。相反,視圖控制器通常以下面的方式處理事件:

  • 視圖控制器為處理高級別事件定義動作方法。動作方法響應:
  • 特定的動作。控件和一些視圖調用動作方法來報告特定的交互。
  • 手勢識別器。手勢識別器調用動作方法報告手勢的當前狀態。使用視圖控制器處理狀態變化或者響應完成的手勢。
  • 視圖控制器監聽系統或其它對象發送的通知。通知報告變化,并且是視圖控制器更新狀態的一種方式。
  • 視圖控制器作為其它對象的數據源或代理。視圖控制器通常用來管理表格和集合視圖的數據。也可以用做對象的代理,例如CLLocationManager對象,該對象發送更新的位置值給它的代理。

響應事件通常涉及更新視圖的內容,這需要有指向這些視圖的引用。視圖控制器是定義之后需要修改的任何視圖的outlet的好地方。使用列表4-1所示的語法聲明outlet為屬性。列表中的自定義類定義了兩個outlets(由IBOutlet關鍵字指定),以及一個動作方法(由IBAction返回類型指定)。Outlet存儲故事版中按鈕和文件框的引用,動作方法響應按鈕點擊事件。

列表4-1 在視圖控制器類中定義outlet和action

OBJECTIVE-C
@interface MyViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *myButton;
@property (weak, nonatomic) IBOutlet UITextField *myTextField;
 
- (IBAction)myButtonAction:(id)sender;
 
@end

SWIFT
class MyViewController: UIViewController {
    @IBOutlet weak var myButton : UIButton!
    @IBOutlet weak var myTextField : UITextField!
    
    @IBAction func myButtonAction(sender: id)
}

記得在故事版中連接視圖控制器的outlet和action到適當的視圖。在故事版文件中連接outlet和action確保視圖加載時,它們已經配置好了。關于如何在界面生成器中創建outlet和action,請參考”Interface Builder Connections Help“。關于如何在應用程序中處理事件,參考”Event Handling Guide for iOS“。

1.3 運行時顯示視圖

故事版讓加載和顯示視圖控制器的視圖變得簡單。需要時,UIKit自動從故事版文件中加載視圖。UIKit執行以下任務序列作為加載過程的一部分:

  1. 使用故事版文件中的信息實例化視圖。
  2. 連接所有outlet和action。
  3. 指定視圖控制器的view屬性為根視圖。
  4. 調用視圖控制器的awakeFromNib方法。
    調用該方法時,視圖控制器的特征集合為空,視圖可能不在最終的位置。
  5. 調用視圖控制器的viewDidLoad方法。
    使用該方法添加或移除視圖,修改布局約束,加載視圖的數據。

在屏幕上顯示視圖控制器的視圖之前,UIKit在視圖顯示在屏幕上之前和之后,提供額外的機會來準備這些視圖。具體來說,UIKit執行以下任務序列:

  1. 調用視圖控制器的viewWillAppear:方法,告訴視圖控制器視圖即將在屏幕上顯示。
  2. 更新視圖的布局。
  3. 在屏幕上顯示視圖。
  4. 視圖出現在屏幕上時,調用viewDidAppear:方法。

添加,移除,或修改視圖的尺寸和位置時,記得添加和移除用于這些視圖的約束。對視圖層級結構做布局相關的改變,會導致布局混亂。在下一個更新周期,布局引擎使用當前布局約束計算視圖的尺寸和位置,并應用這些變化到視圖層級結構。

關于如何不使用故事版創建視圖,請參考“UIViewController Class Reference”中的視圖管理信息。

1.4 管理視圖布局

當視圖的尺寸和位置改變時,UIKit更新視圖層級結構的布局信息。對于使用自定布局(Auto Layout)配置的視圖,UIKit使用自動布局引擎根據當前約束更新布局。同時UIKit通知其它感興趣的對象(例如當前顯示的控制器),布局發生了改變,以便它們做出響應。

布局過程中,UIKit在幾個點上發出通知,讓你可以執行布局相關的任務。使用這些通知來修改布局約束,或者在布局約束應用后,做最后的布局調整。布局過程中,UIKit為每個受影響的視圖控制器做以下事情:

  1. 如果需要,更新視圖控制器和它的視圖的特征集合。參考“什么時候發生特征和尺寸變化?”。
  2. 調用視圖控制器的viewWillLayoutSubviews方法。
  3. 調用當前UIPresentationController對象的containerViewWillLayoutSubviews方法。
  4. 調用視圖控制器根視圖的layoutSubviews。該方法默認使用可用的約束來計算新的布局信息。然后遍歷視圖層級結構,并調用每個子視圖的layoutSubviews方法。
  5. 應用計算好的布局信息到視圖。
  6. 調用視圖控制器的viewDidLayoutSubviews方法。
  7. 調用當前UIPresentationController對象的containerViewDidLayoutSubviews方法。

視圖控制器可用使用viewWillLayoutSubviews和viewDidLayoutSubviews方法執行額外的更新,這些更新可能會影響布局過程。布局之前,添加或移除視圖,更新視圖的尺寸或位置,更新約束,或者更新其它視圖相關的屬性。布局之后,可以重新加載表格數據,更新其它視圖的內容,或者調整視圖最終的尺寸和位置。

以下是高效管理布局的技巧:

  • 使用自動布局。使用自動布局創建的約束是一種靈活和簡單的方式,可用在不同屏幕尺寸上放置內容。
  • 利用頂部和底部的布局參考線。這些參考線確保內容總是可見的。頂部布局參考線的位置把狀態欄和導航欄的高度計算在內,底部布局參考線把標簽欄或工作欄的高度計算在內。
  • 添加或移除視圖時,記得更新約束。如果動態的添加或移除視圖,記得更新相應的約束。
  • 視圖控制器的視圖產生動畫時,暫時的移除約束。當使用UIKit核心動畫(Core Animation)讓視圖產生動畫時,在動畫期間移除約束,并在動畫完成后添加回來。如果動畫期間視圖的位置或尺寸發生了變化,記得更新約束。

關于顯示控制器和它們在視圖控制器架構中扮演的角色,請參考“彈出和過渡過程”。

1.5 高效的管理內存

絕大部分的內存分配工作由你來決定,表格4-1列出了UIViewController的方法,最有可能在這些方法中分配或釋放內存。絕大部分釋放內存都涉及移除對象的強引用。通過設置指向該對象的屬性和變量為nil來移除對象的強引用。

表格4-1 分配和釋放內存的地方

任務 方法 討論
分配視圖控制器需要的關鍵數據結構。 初始化方法 自定義的初始化方法(無論名稱是否為init)總是負責把視圖控制器對象放到一個已知的狀態。使用這些方法來分配任何需要的數據結構,確保完成適當的操作。
分配或加載視圖中顯示的數據。 viewDidLoad 使用該方法加載所有要顯示的數據對象。該方法調用時,視圖對象已經存在,并且處于已知的狀態。
響應低內存通知。 didReceiveMemoryWarning 使用該方法釋放所有與視圖控制器相關的非關鍵對象。盡可能多的釋放內存。
釋放視圖控制器需要的關鍵數據結構。 dealloc 覆寫的該方法只執行視圖控制器類最后的清理工作。系統自動釋放存儲在實例變量和屬性中的對象,所以不需要顯式的是釋放它們。

2 實現容器視圖控制器

容器視圖控制器是組合多個視圖控制器內容到一個用戶界面的一種方式。容器視圖控制器最常用于導航和基于現有內容創建新的用戶界面類型。UIKit中的容器視圖控制器包括UINavigationController,UITabBarController和UISplitViewController,所有這些都方便在用戶界面的不同部分導航。

2.1 設計自定義容器視圖控制器

容器視圖控制器幾乎在所有方面都與內容視圖控制器相似,它管理一個根視圖和一些內容。不同的是,容器視圖控制器的部分內容來源于其它視圖控制器。它獲得的內容僅限于其它視圖控制器的視圖,該視圖嵌入到容器視圖控制器自己的視圖層級結構中。容器視圖控制器設置嵌入視圖的尺寸和位置,但原始的視圖控制器仍然管理這些視圖中的內。

設計自己的容器視圖控制器時,需要理解容器和被包含的視圖控制器之間的關系。視圖控制器之間的關系可以幫助了解它們的內容如何在屏幕上顯示,以及容器內部如何管理它們。設計過程中,詢問自己以下問題:

  • 容器和它的子視圖控制器分別扮演什么角色?
  • 同時顯示多少個子視圖控制器?
  • 兄弟視圖控制器之間的關系(如果存在的話)是什么?
  • 子視圖控制器如何從容器中添加或移除?
  • 子視圖控制器的尺寸或位置能否改變?什么條件下發生這些改變?
  • 容器本身是否提供裝飾或導航相關的視圖?
  • 容器和子視圖控制器之間的通信方式是什么?除了UIViewController類定義的標準事件,容器還需要發送特定的事件到子視圖控制器嗎?
  • 容器的外觀能否以不同的方式配置?如果可以,怎么配置?

定義了各種對象之后,容器視圖控制器的實現變得相對簡單了。UIKit的唯一要求是在容器視圖控制器和子視圖控制器之間建立正式的父子關系。父子關系確保子視圖控制器可以接收任何相關的系統消息。除此之外,大部分的實際工作發生在布局和管理被包含的視圖中,并且不同的容器有不同的工作。可以把視圖放在容器內容區域的任何位置,并設計你需要的尺寸。還可以添加自定義視圖到視圖層級結構中,用于提供裝飾或者幫助導航。

2.1.1 例子:導航控制器

UINavigationController對象支持在層級數據集中導航。導航界面一次顯示一個子視圖控制器。界面頂部的導航欄顯示數據的層級結構和一個返回上一級的按鈕。導航到數據層級結構下一級由子視圖控制器決定,可以使用表格或按鈕。

視圖控制器之間的導航由導航控制器和子視圖控制器共同管理。當用戶與按鈕或子視圖控制器的表格行交互時,子視圖控制器請求導航控制器入棧一個新的視圖控制器到視圖。子視圖控制器處理新視圖控制器內容的配置,而導航控制器管理過渡動畫。導航控制器還管理導航欄,顯示一個用于關閉(dismiss)頂層視圖控制器的按鈕。

圖5-1展示了導航控制器和它的視圖的結構。頂層子視圖控制器填充大部分內容區域,導航欄占據一小部分。

圖5-1 導航界面的結構

在緊湊和常規環境中,導航控制器一次只顯示一個子視圖控制器。導航控制器調整子視圖控制器尺寸來適應可用的空間。

2.1.2 例子:分割視圖控制器

UISplitViewController對象在主從排列中顯示兩個視圖控制器的內容。這種排列中,其中一個視圖控制器(主控制器)決定了另外一個視圖控制器顯示的細節。兩個視圖控制器是否可見是可配置的,但也由當前環境決定。在水平方向為常規環境中,分割視圖控制器并排顯示兩個子視圖控制器,或者根據需要顯示或者隱藏主視圖控制器。在緊湊環境中,分割視圖控制器一次只顯示一個視圖控制器。

圖5-2展示了水平方向為常規環境時,分割視圖界面和它的視圖的結構。默認情況下,分割視圖控制器本身只有自己的容器視圖。在這個例子中,兩個子視圖并排顯示。子視圖的尺寸和主視圖是否可見都是可配置的。

圖5-2 分割視圖界面

2.2 在界面生成器中配置容器

設計時,通過添加容器視圖對象到故事版場景中來創建父子容器關系,如圖5-3所示。容器視圖對象是一個占位對象,表示子視圖控制器的內容。使用該視圖控制子視圖控制器的根視圖與容器中其它視圖相關的尺寸和位置。

圖5-3 在界面生成器中添加容器視圖

加載有一個或多個容器視圖的視圖控制器時,界面生成器也加載這些視圖相關的子視圖控制器。子視圖控制器必須與父視圖控制器同時實例化,這樣才能創建適當的父子關系。

如果不使用界面生成器設置父子容器關系,就必須通過代碼添加每個子視圖控制器到容器視圖控制器來創建這些關系,請參考”添加子視圖控制器到內容“。

2.3 實現自定義容器視圖控制器

要實現一個容器視圖控制器,必須在視圖控制器和子視圖控制器之間建立關系。必須在管理任何子視圖控制器的視圖之前建立這些父子關系。這樣才能讓UIKit知道視圖控制器正在管理其孩子的尺寸和位置。可以在界面生成器或者通過代碼創建這種關系。通過代碼創建父子關系時,必須在設置視圖控制器時,顯式的添加和移除子視圖控制器。

2.3.1 添加子視圖控制器到內容

要把子視圖控制器通過代碼合并到內容中,需要完成以下幾點來創建相關視圖控制器之間的父子關系:

  1. 調用容器視圖控制器的addChildViewController:方法。該方法告訴UIKit,容器視圖控制器正在管理子視圖控制器的視圖。
  2. 添加子視圖控制器的根視圖到內容器的視圖層級結構中。在這個過程中,需要設置子視圖的frame的尺寸和位置。
  3. 添加約束來管理子視圖控制器根視圖的尺寸和位置。
  4. 調用子視圖控制器的didMoveToParentViewController:方法。

列表5-1展示了容器如何嵌入子視圖控制器。建立父子關系后,容器設置子視圖控制器的frame,并將子視圖控制器的視圖添加到容器的視圖層級結構中。設置子視圖的frame尺寸確保視圖在容器中正確的顯示。添加視圖后,容器調用子視圖控制器的didMoveToParentViewController:方法,讓子視圖控制器可以響應視圖的變化。

列表5-1 添加子視圖控制到到容器中

- (void) displayContentController: (UIViewController*) content {
   [self addChildViewController:content];
   content.view.frame = [self frameForContentController];
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];
}

在上面的例子中,只調用了子視圖控制器的didMoveToParentViewController:方法。這是因為addChildViewController:方法調用了子視圖控制器的willMoveToParentViewController:方法。必須自己調用didMoveToParentViewController:方法是因為該方法直到子視圖控制器的視圖嵌入容器的視圖層級結構后才會被調用。

使用自動布局時,在添加子視圖到容器的視圖層級結構之后,才設置容器和子視圖控制器之間的約束。約束應該只影響子視圖控制器的根視圖的尺寸和位置。不要改變根視圖的內容或子視圖層級結構中的任何視圖。

2.3.2 移除子視圖控制器

想從內容中移除子視圖控制器,通過完成以下幾點來移除視圖控制器之間的父子關系:

  1. 使用nil值調用子視圖控制器的willMoveToParentViewController:方法。
  2. 移除子視圖控制器的根視圖配置的所有約束。
  3. 從內容的視圖層級結構中移除子視圖控制器的根視圖。
  4. 調用子視圖控制器的removeFromParentViewController方法終止父子關系。

移除子視圖控制器會永久的刪除父子之間的關系。只有不再需要子視圖控制器時才移除它。例如,當新視圖控制器壓入導航棧時,導航控制器不會移除當前的子視圖控制器。只有在它們從棧中彈出時才會移除。

列表5-2展示了如何從容器中移除子視圖控制器。使用nil值調用willMoveToParentViewController:方法,讓子視圖控制器有機會準備這個變化。removeFromParentViewController方法也調用了子視圖控制器的didMoveToParentViewController:方法,傳入的參數值為nil。設置父視圖控制器為nil完成從容器中移除子視圖控制器的視圖。

列表5-2 從容器中移除子視圖控制器

- (void) hideContentController: (UIViewController*) content {
   [content willMoveToParentViewController:nil];
   [content.view removeFromSuperview];
   [content removeFromParentViewController];
}

2.3.3 子視圖控制器之間的過渡

想要動畫的用一個子視圖控制器替換另一個,需要合并子視圖控制器的添加和移除到過渡動畫過程中。動畫開始前,確保兩個子視圖控制器都是內容的一部分,同時讓當前的子視圖控制器知道它即將消失。動畫過程中,移動新的子視圖到相應的位置,并移除舊的子視圖。動畫完成后,完全移除子視圖控制器。

列表5-3展示了如何使用過渡動畫切換兩個子視圖控制器。這個例子中,新的視圖控制器動畫的移動到現有子視圖控制器當前占據的矩形,而子視圖控制器移出屏幕。動畫完成后,完成塊代碼從容器中移出子視圖控制器。這個例子中,transitionFromViewController:toViewController:duration:options:animations:completion:方法自動更新容器的視圖層級結構,所以不需要自己添加和移出視圖。

列表5-3 兩個子視圖控制器之間的過渡

- (void)cycleFromViewController: (UIViewController*) oldVC
               toViewController: (UIViewController*) newVC {
   // Prepare the two view controllers for the change.
   [oldVC willMoveToParentViewController:nil];
   [self addChildViewController:newVC];
 
   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.
   newVC.view.frame = [self newViewStartFrame];
   CGRect endFrame = [self oldViewEndFrame];
 
   // Queue up the transition animation.
   [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
            // Animate the views to their final positions.
            newVC.view.frame = oldVC.view.frame;
            oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
           // Remove the old view controller and send the final
           // notification to the new view controller.
           [oldVC removeFromParentViewController];
           [newVC didMoveToParentViewController:self];
        }];
}

2.3.4 管理子視圖控制器的顯示更新

添加子視圖控制器到容器后,容器自動轉發顯示相關的消息到子視圖控制器。通常這是你希望的行為,因為它確保所有事件都正確發送。然后,有時默認行為發送這些事件的順序對容器沒有意義。例如,如果多個子視圖控制器同時改變視圖的狀態,可能希望合并這些變化,這樣顯示的回調函數能以更有邏輯的順序同時出現。

想要接管顯示回調的職責,需要在容器視圖控制器中覆寫shouldAutomaticallyForwardAppearanceMethods,并返回NO,如圖5-4所示。

列表5-4 禁用自動轉發顯示

- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
    return NO;
}

顯示過渡發生時,適當的調用子視圖控制器的beginAppearanceTransition:animated:endAppearanceTransition方法。例如,如果容器的child屬性只有一個子視圖控制器引用,容器可以轉發這些消息到該子視圖控制器,如列表5-5所示。

列表5-5 容器顯示或消失時轉發顯示消息

-(void) viewWillAppear:(BOOL)animated {
    [self.child beginAppearanceTransition: YES animated: animated];
}
 
-(void) viewDidAppear:(BOOL)animated {
    [self.child endAppearanceTransition];
}
 
-(void) viewWillDisappear:(BOOL)animated {
    [self.child beginAppearanceTransition: NO animated: animated];
}
 
-(void) viewDidDisappear:(BOOL)animated {
    [self.child endAppearanceTransition];
}

2.4 構建容器視圖控制器的建議

設計,開發和測試新的容器視圖控制器需要時間。盡管單個行為很簡單,但控制器作為整體變得很復雜。實現自己的容器類時,考慮以下技巧:

  • 只訪問子視圖控制器的根視圖。容器應該只訪問每個子視圖控制器的根視圖,也就是子視圖控制器的view屬性返回的視圖。永遠不要訪問子視圖控制器的其它視圖。
  • 子視圖控制器應該盡可能少的了解它們的容器。子視圖控制器應該關注自身的內容。如果容器允許子視圖控制器影響它的行為,應該使用代理設計模式來管理這些交互。
  • 優先使用常規視圖設計容器。使用常規視圖(而不是子視圖控制器的視圖)讓你可以在簡單的環境中測試布局約束和過渡動畫。當常規視圖按預期工作后,移除這些視圖,并切換為子視圖控制器的視圖。

2.5 委托控制給子視圖控制器

容器視圖控制器可以委托一些自身的外觀給一個或多個子視圖控制器。可以通過以下幾種方式委托控制:

  • 讓子視圖控制器決定狀態欄風格。在容器視圖控制器中覆寫其中一個或兩個childViewControllerForStatusBarStyle和childViewControllerForStatusBarHidden方法,委托狀態欄風格給子視圖控制器。
  • 讓子視圖控制器指定自己合適的尺寸。靈活布局的容器可以使用子視圖控制器的preferredContentSize屬性,來幫助決定子視圖的尺寸。

3 支持輔助功能(Accessibility)

4 保存和恢復狀態

視圖控制器在狀態保存和恢復過程中扮演重要的角色。應用程序掛起前,狀態保存記錄了應用的配置,因此隨后啟動時可以恢復該配置。恢復應用到上一個配置可以為用戶節約時間,并提供更好的用戶體驗。

保存和恢復過程幾乎是自動完成的,但需要告訴iOS需要保存應用程序的哪些部分。保存視圖控制器的步驟如下:

  • (必須)為需要保存配置的視圖控制器指定恢復標識符;參考”為保存標記視圖控制器“。
  • (必須)啟動時告訴iOS,如何創建或定位新的視圖控制器對象;參考”啟動時恢復視圖控制器“。
  • (可選)對每個視圖控制器,存儲用來返回視圖控制器到原始配置的任何指定配置數據;參考”編碼和解碼視圖控制器的狀態“。

保存和恢復過程的概述,請參考”App Programming Guide for iOS“。

4.1 為保存標記視圖控制器

UIKit只會保存你讓它保存的視圖控制器。每個視圖控制器有一個restorationIdentifier屬性,該屬性默認值為nil。設置該屬性為一個有效的字符串,告訴UIKit應該保存該視圖控制器和它的視圖。可以通過代碼或故事版文件指定恢復標識符。

指定恢復標識符時,視圖控制器層級結構中所有的父視圖控制器也必須有恢復標識符。在保存過程中,UIKit從窗口的根視圖控制器開始遍歷視圖控制器層級結構。如果該層級結構中的一個視圖控制器沒有恢復標識符,則該視圖控制器,它的所有子視圖控制器,以及presented視圖控制器都會被忽略。

4.1.1 選擇有效的恢復標識符

UIKit使用恢復標識符字符串在之后重新創建視圖控制器,所以需要選擇一個代碼容易識別的字符串。如果UIKit不能自動創建視圖控制器,它會提供該視圖控制器和它所有父視圖控制器的恢復標識符,讓你來創建。該標識符鏈代表視圖控制器的恢復路徑,以及如何決定被請求的是哪個視圖控制器。恢復路徑從根視圖控制器開始,包括每一個視圖控制器和被請求的視圖控制器。

恢復標識符通常是視圖控制器的類名。如果在很多地方使用了同一個類,也許希望指定一個更有意義的值。例如,基于視圖控制器管理的數據來指定一個字符串。

每一個視圖控制器的恢復路徑必須是唯一的。如果容器視圖控制器有兩個子視圖控制器,容器必須為每一個子視圖控制器指定唯一的恢復標識符。一些UIKit中的容器視圖控制器能自動消除子視圖控制器的歧義,允許子視圖控制器使用相同的恢復標識符。例如,UINavigationController類根據每個子視圖控制器在導航棧中的位置添加信息到子視圖控制器。關于給定的視圖控制器的行為的更多信息,請查看相應的類參考。

更多如何使用恢復標識符和恢復路徑創建視圖控制器的信息,請參考”啟動時恢復視圖控制器“。

4.1.2 排除視圖控制器組

通過設置父視圖控制器的恢復標識符為nil,從恢復過程中排除整個視圖控制器組。圖7-1展示了設置恢復標識符為nil對視圖控制器層級結構的影響。缺少保存數據,阻止了視圖控制器稍后的恢復。

圖7-1 從自動保存過程中排除視圖控制器

排除一個或多個視圖控制器,并不會在隨后的恢復中移除它們。啟動時,作為應用程序默認設置部分的所有視圖控制器仍然會創建,如圖7-2所示。這些視圖控制器按照默認配置重新創建。

圖7-2 加載默認視圖控制器集

從自動保存過程中排除的視圖控制器,可以手動保存。通過在恢復文件(restoration archive)中保存視圖控制器的引用,可以保存該視圖控制器和它的狀態信息。例如,如果圖7-1中的應用程序代理保存了導航控制器的三個子視圖控制器,它們的狀態將會被保存。在恢復過程中,應用程序代理可以重新創建它們,并把它們壓入導航控制器的棧中。

4.1.3 保存視圖控制器的視圖

有些視圖有額外的跟視圖相關的狀態信息,而不是跟父視圖控制器相關。例如,你希望保存滾動視圖的滾動位置。視圖控制器負責為滾動視圖提供內容,滾動視圖本身負責保存視覺狀態。

執行以下操作來保存視圖的狀態:

  • 為視圖的restorationIdentifier屬性指定一個有效的字符串。
  • 使用同樣具有有效恢復標識符的視圖控制器的視圖。
  • 對于表格視圖和集合視圖,指定一個遵循UIDataSourceModelAssociation協議的數據源。

為視圖指定恢復標識符告訴UIKIt應該把視圖的狀態寫入保存文件。稍后恢復視圖控制器時,UIKit也恢復具有恢復標識符的所有視圖的狀態。

4.2 啟動時恢復視圖控制器

啟動時,UIKit視圖恢復應用程序到上一個狀態。此時,UIKit請求應用程序創建(或定位)視圖控制器對象,該對象包括保存的用戶界面。定位視圖控制器時,UIKit按以下順序搜索:

  1. 如果視圖控制器有恢復類,UIKit請求該類提供視圖控制器。UIKit調用關聯的恢復類的viewControllerWithRestorationIdentifierPath:coder:方法來檢索視圖控制器。如果該方法返回nil,則假設應用程序不希望重新創建視圖控制器,UIKit停止搜索視圖控制器。
  2. 如果視圖控制器沒有恢復類,UIKit請求應用程序代理提供視圖控制器。UIKit調用應用程序代理的application:viewControllerWithRestorationIdentifierPath:coder:方法搜索沒有恢復類的視圖控制器。如果該方法返回nil,UIKit嘗試隱式查找視圖控制器。
  3. 如果有正確恢復路徑的視圖控制器已經存在,UIKit使用該對象。如果應用程序在啟動時創建視圖控制器(通過代碼或從故事版中加載),并且這些視圖控制器有恢復標識符,UIKit根據它們的恢復路徑隱式的找到它們。
  4. 如果視圖控制器最初從故事版文件中加載,UIKit使用保存的故事版信息來定位并創建它。UIKit在恢復文件中保存視圖控制器的故事版信息。如果恢復時,其它方式沒有查找到視圖控制器,UIKit使用該信息定位同一個故事版,并實例化相應的視圖控制器。

為視圖控制器指定一個恢復類,可以阻止UIkit隱式的搜索該視圖控制器。使用恢復類讓你可以進一步控制是否真的想要創建視圖控制器。例如,如果恢復類決定視圖控制器不應該重新創建,viewControllerWithRestorationIdentifierPath:coder:方法可以返回nil。當不存在恢復類時,UIKit盡可能找到或創建視圖控制器并恢復它。

使用恢復類時,viewControllerWithRestorationIdentifierPath:coder:方法應該創建新的類實例,執行最小化的初始化,并返回對象。列表7-1展示了如何使用該方法從故事版中加載視圖控制器。因為視圖控制器最初從故事版中加載,所有該方法使用UIStateRestorationViewControllerStoryboardKey關鍵字從文件中獲得故事版。該方法不會試圖配置視圖控制器的數據字段。該步驟發生在解碼視圖控制器的狀態之后。

列表7-1 恢復中創建新的視圖控制器

+ (UIViewController*) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
                      coder:(NSCoder *)coder {
   MyViewController* vc;
   UIStoryboard* sb = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
   if (sb) {
      vc = (PushViewController*)[sb instantiateViewControllerWithIdentifier:@"MyViewController"];
      vc.restorationIdentifier = [identifierComponents lastObject];
      vc.restorationClass = [MyViewController class];
   }
    return vc;
}

手動重新創建視圖控制器時,重新分配恢復標識符是一個好習慣。恢復恢復標識符最簡單的方式是獲得identifierComponents數組的最后一項,并分配給視圖控制器。

啟動時從應用程序主故事版中創建的對象,不要為每個對象創建新的實例。讓UIKit隱式的查找這些對象,或使用應用程序代理的application:viewControllerWithRestorationIdentifierPath:coder:方法查找已經存在的對象。

4.3 編碼和解碼視圖控制器的狀態

UIKit調用每一個要保存對象的encodeRestorableStateWithCoder:方法來保存對象的狀態。恢復過程中,UIKit調用匹配的decodeRestorableStateWithCoder:方法解碼狀態,并應用到對象。對于視圖控制器來說,這些方法的實現是可選的,但推薦實現。可以使用它們保存和恢復以下類型的信息:

  • 被顯示的數據的引用(不是數據本身)
  • 對于容器視圖控制器,指向子視圖控制器的引用
  • 當前選中的信息
  • 對于有用戶可配置的視圖的視圖控制器,該視圖的當前配置信息。

在編碼和解碼方法中,可以編碼任何編碼器支持的對象和數據類型。除了視圖和視圖控制器之外的所有對象必須遵循NSCoding協議,并使用協議中方法保存狀態。編碼器不使用NSCoding協議保存視圖和視圖控制器的狀態。相反,編碼器保存對象的恢復標識符,并將其添加到保存對象列表,這將導致該對象的encodeRestorableStateWithCoder:方法被調用。

視圖控制器的encodeRestorableStateWithCoder:和decodeRestorableStateWithCoder:方法必須在實現中調用super。調用super可以讓父類保存和恢復額外的信息。列表7-2展示了這些方法的簡單實現,其中保存了一個數值用于識別指定的視圖控制器。

圖7-2 編碼和解碼視圖控制器的狀態

- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
   [super encodeRestorableStateWithCoder:coder];
 
   [coder encodeInt:self.number forKey:MyViewControllerNumber];
}
 
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
   [super decodeRestorableStateWithCoder:coder];
 
   self.number = [coder decodeIntForKey:MyViewControllerNumber];
}

編碼和解碼過程中,編碼器對象不是共享的。每個保存狀態的對象接收自己的編碼器對象。使用唯一的編碼器意味著不用擔心關鍵字之間的命名空間沖突。不要自己使用UIApplicationStateRestorationBundleVersionKey,UIApplicationStateRestorationUserInterfaceIdiomKey和UIStateRestorationViewControllerStoryboardKey關鍵字名。UIKit使用這些關鍵字存儲視圖控制器額外的信息。

關于視圖控制器編碼解碼方法的更多信息,請參考“UIViewController Class Reference”。

4.4 保存和恢復視圖控制器的技巧

在視圖控制器中添加狀態保存和恢復時,考慮以下指南:

  • 請記住你可能不希望保存所有視圖控制器。某些情況下,保存一個視圖控制器可能沒有意義。例如,如果應用程序正在顯示一個錢幣兌換,你可能希望取消操作,并回到前一個界面。這種情況下,不應該保存視圖控制器,而應該請求新的密碼信息。
  • 恢復過程中避免交換視圖控制器類。狀態保存系統編碼保存的視圖控制器類。恢復過程中,如果應用程序返回一個不匹配(或者不是它的一個子類)原始對象的對象,系統不會請求視圖控制器解碼任何狀態信息。因此,交換舊視圖控制器為一個完全不同的視圖控制器,不會恢復對象的全部狀態。
  • 狀態保存系統希望你按預期使用視圖控制器。恢復過程依賴視圖控制器之間的包含關系來重新構建界面。如果沒有正確使用容器視圖控制器,保存系統就會找不到視圖控制器。例如,永遠不要把一個視圖控制器的視圖嵌入一個不同的視圖,除非對應的視圖控制器之間存在包含關系。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容