UIViewController(二):視圖控制器定義

子類化視圖控制器

設計UI

可以使用Xcode中的storyboard文件直觀地定義視圖控制器的UI,也可以以編程方式來創建UI。但storyboard可以將視圖的內容可視化,并根據需要自定義視圖層次結構。以可視化方式構建UI界面,可以讓我們快速進行更改并能看到結果,而無需構建和運行應用程序。

下圖顯示了一個storyboard的例子。每個矩形區域代表一個視圖控制器及其相關視圖,視圖控制器之間的箭頭是視圖控制器的關系和節點。關系將容器視圖控制器連接到其子視圖控制器。Segue可用于在界面中的視圖控制器之間導航。

圖5-1

處理用戶交互

應用程序的響應者對象會處理傳入的事件,雖然視圖控制器是響應者對象,但它們很少直接處理觸摸事件。相反,視圖控制器通常以下列方式處理事件:

  • 視圖控制器定義了處理更高級別事件的操作方法。行動方法回應:
    • 具體行動。控件和一些視圖調用行動方法來報告特定的交互。
    • 手勢識別器。手勢識別器調用行動方法來報告手勢的當前狀態。使用視圖控制器處理狀態變更或響應已完成的手勢。
  • 視圖控制器觀察由系統或其他對象發出的通知,通知報告更改。通知也是視圖控制器更新其狀態的一種方式。
  • 視圖控制器充當另一個對象的數據源或委托。例如,可以將它們用作CLLocationManager對象的代理,該對象將更新的位置發送給其委托對象。

在運行時顯示視圖

storyboard加載和顯示視圖控制器視圖的過程非常簡單。當需要時,UIKit會自動從stroyboard文件中加載視圖。作為加載過程的一部分,UIKit執行以下任務序列:

  • 使用storyboard文件中的信息實例化視圖。
  • 連接所有的outlets和actions。
  • 將根視圖分配給視圖控制器的視圖屬性。
  • 調用視圖控制器的awakeFromNib方法。調用此方法時,視圖控制器的特征集合為空,視圖可能不在其最終位置。
  • 調用視圖控制器的viewDidLoad方法。在此方法中添加或者刪除視圖,修改布局約束,并未視圖加載數據。

在屏幕上顯示視圖控制器的視圖之前,UIKit提供了額外的機會在視圖顯示前后來執行需要的操作。具體來說,UIKit執行以下任務序列:

  • 在視圖即將出現在屏幕上前,調用視圖控制器的viewWillAppear:方法。
  • 更新視圖的布局。
  • 在屏幕上顯示視圖。
  • 視圖顯示后,調用視圖控制器的viewDidAppear:方法。

添加、刪除或修改視圖的尺寸或者位置時,也要添加和刪除適用于這些視圖的任何約束。對視圖層次結構進行與布局相關的更改會導致UIKit將布局標記為臟。在下一個更新周期中,布局引擎使用當前布局約束條件計算視圖的尺寸和位置,并將這些更改應用于視圖層次結構。

管理視圖布局

當視圖的尺寸和位置發生變化時,UIKit將更新視圖層次結構的布局信息。對于使用Auto Layout配置的視圖,UIKit將使用Auto Layout引擎根據當前約束來更新布局。UIKit還會通知其他感興趣的對象(如正在呈現的控制器)布局發生更改,以便它們可以做出相應的響應。

在布局過程中,UIKit會在幾個時間點發出通知,以便我們可以執行其他與布局相關的任務。使用這些用紙來修改布局約束,或者在應用布局約束后對布局進行最終的調整。在布局過程中,UIKit為每個受影響的視圖控制器執行以下操作:

  • 根據需要更新視圖控制器及其視圖的特征集合。
  • 調用視圖控制器的viewWillLayoutSubviews方法。
  • 調用當前UIPresentationControllercontainerViewWillLayoutSubviews方法。
  • 調用視圖控制器的根視圖的layoutSubviews方法。此方法默認使用可用的約束來計算新的布局信息。然后該方法遍歷視圖層,并調用每個子視圖的layoutSubviews方法。
  • 將計算的布局信息應用于視圖。
  • 調用視圖控制器的viewDidLayoutSubviews方法。
  • 調用當前UIPresentationController對象的containerViewDidLayoutSubviews方法。

視圖控制器可以使用viewWillLayoutSubviewsviewDidLayoutSubviews方法來執行可能影響布局過程的附加更新。在布局之前,可以添加或刪除視圖,更新視圖的尺寸或位置,更新約束或更新其他視圖相關的屬性。布局之后,可以重新加載表格數據,更新其他視圖的內容,或對視圖的尺寸和位置進行最終調整。

實現一個容器視圖控制器

容器視圖控制器是將多個視圖控制器的內容合并到單個用戶界面中的一種方法。容器視圖控制器通常使導航變得容易,并基于現有內容創建新的用戶界面類型。UIKit中容器視圖控制器的示例包括UINavigationControllerUITabBarControllerUISplitViewController,它們都可以方便在用戶界面的不同部分之間進行導航。

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

幾乎在任何情況下,容器視圖控制器都像其他任何內容視圖控制器一樣管理根視圖和一些內容。區別在于容器視圖控制器從其他視圖控制器獲取其內容的一部分。其獲取的內容僅限于其他視圖控制器的視圖,這些視圖嵌入在其自己的根視圖層次結構中。容器視圖控制器設置任何嵌入視圖的尺寸和位置,但原始視圖控制器仍然管理這些視圖內的內容。

在設計容器視圖控制器時,需要始終了解容器和包含的視圖控制器之間的關系。視圖控制器的關系可以幫助告知它們的內容應該如何顯示在屏幕上,以及容器如何在內部管理它們。在設計過程中,需要搞清楚以下幾個問題:

  • 容器的作用是什么?其子視圖控制器扮演什么樣的角色?
  • 多少個子視圖控制器同時顯示?
  • 同級子視圖控制器之間有什么關系(如果有的話)?
  • 子視圖控制器如何添加到容器或從容器中移除?
  • 子視圖控制器的尺寸和位置能改變嗎?這些變化在什么情況下發生?
  • 容器是否提供任何裝飾或導航相關的視圖?
  • 容器視圖控制器和子視圖控制器之間如何通信?容器是否需要向UIViewController類定義的標準事件報告特定的事件?
  • 容器的外觀是否可以用不同的方式配置?如果可以,如何實現?

在定義了各種對象的角色之后,容器視圖控制器的實現相對簡單。UIKit唯一的要求就是在容器視圖控制器和任何子視圖控制器之間建立正式的父子關系。父子關系確保子視圖控制器收到任何相關的系統消息。除此之外,大部分的實際工作都是在包含視圖的布局和管理過程中發生的,每個容器都是不同的。我們可以將視圖放置在容器的內容區域的任何位置,然后根據需要調整視圖的尺寸。還可以將自定義視圖添加到視圖層次結構中,以提供裝飾或者輔助導航。

示例:導航控制器

UINavigationController對象支持通過分層數據集來導航。導航界面一次顯示一個子視圖控制器。界面頂部的導航欄顯示數據層次結構中當前位置,并顯示后退按鈕以向后移動一個級別。向下導航到另一個子視圖控制器,并將子視圖控制器添加到數據層次結構中。

視圖控制器之間的導航由導航控制器及其子視圖控制器共同管理。當用戶與子視圖控制器的按鈕或表格進行交互后,子視圖控制器要求導航控制器將新的視圖控制器推入視圖。子視圖控制器處理新的視圖控制器的內容的配置,但導航控制器管理轉場動畫。導航控制器還管理導航欄,該導航欄顯示用于解除最頂層視圖控制器的后退按鈕。

下圖顯示了導航控制器及其視圖的結構。大多數內容區域由最頂層的子視圖控制器填充,只有一小部分被導航欄占用。

圖6-1

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

示例:分割控制器

UISplitViewController對象以主要-細節排列方式來顯示兩個視圖控制器的內容。在這種排列中,一個視圖控制器(主視圖)的內容決定了其他視圖控制器顯示的細節。兩個視圖控制器的可見性是可配置的,但也受到當前環境的支配。在規則的水平環境中,分割視圖控制器可以同時顯示兩個子視圖控制器,或者可以隱藏主視圖控制器并根據需要顯示。在緊湊的環境中,分割視圖控制器一次只顯示一個視圖控制器。

下圖顯示了在一個常規的水平環境中的分割視圖界面及其視圖的結構。分割視圖控制器本身只有默認的容器視圖。在這個例子中,兩個子視圖是并排顯示的。子視圖的尺寸是可配置的,主視圖的可見性也是可配置的。

圖6-2

在Interface Builder中配置容器

要在設計時創建父子容器關系,需要將一個容器視圖對象添加到視圖控制器中,如下圖所示。容器視圖對象是代表子視圖控制器內容的占位符對象。使用該視圖來調整和定位與容器中其他視圖相關的子視圖。

圖6-3

當使用一個或多個容器視圖加載視圖控制器時,Interface Builder還會加載與這些視圖關聯的子視圖控制器。子視圖控制器必須與父視圖控制器同時實例化,以便建立適當的父子關系。

如果不使用Interface Builder來設置父 - 子容器關系,則必須通過將每個子項添加到容器視圖控制器來以編程方式創建這些關系。

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

要實現一個容器視圖控制器,必須建立容器視圖控制器和其子視圖控制器之間的關系。在嘗試管理任何子視圖控制器的視圖之前,建立這些父子關系是必需的。這樣做讓UIKit知道容器視圖控制器正在管理子視圖控制器的尺寸和位置。我們可以在Interface Builder中創建這些關系,或以編程方式創建它們。

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

要以編程方式將子視圖控制器合并到內容中,請執行以下操作,在相關的視圖控制器之間創建父子關系:

  • 調用容器視圖控制器的addChildViewController:方法,此方法告訴UIKit容器視圖控制器現在正在管理子視圖控制器的視圖。
  • 設置好子視圖控制器內容的尺寸和位置,將子視圖控制器的根視圖添加到容器視圖控制器的視圖層次結構中。
  • 添加任何約束來管理子視圖控制器的根視圖的尺寸和位置。
  • 調用子視圖控制器的didMoveToParentViewController:方法。

以下代碼展示了如何在容器視圖控制器中嵌入一個子視圖控制器。建立父子關系后,容器視圖控制器設置其子視圖控制器內容的框架,并將子視圖控制器的根視圖添加到自己的視圖層次結構中。設置子視圖控制器的根視圖的尺寸很重要,能夠確保視圖在容器中正確顯示。在添加視圖之后,容器視圖控制器調用子視圖控制器的didMoveToParentViewController:方法,以使子視圖控制器有機會響應視圖所有權的更改。

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

注意,在上面的例子中,只調用了子視圖控制器的didMoveToParentViewController:方法。容器視圖控制器的addChildViewController:方法只會調用子視圖控制器的willMoveToParentViewController:方法,我們必須自己手動調用子視圖控制器的didMoveToParentViewController:。因為只有在將子視圖嵌入容器的視圖層次結構中后,才能調用此方法。

使用自動布局時,在將子對象添加到容器的視圖層次結構后,在容器和子對象之間設置約束。添加的約束只會影響子視圖控制器的根視圖的尺寸和位置。請勿直接更改子視圖層次結構中的根視圖或任何其他視圖的內容。

移除子視圖控制器

要從容器視圖控制器中刪除子視圖控制器,可以通過執行以下操作來刪除視圖控制器之間的父子關系:

  • 調用子視圖控制器的willMoveToParentViewController:方法且傳入的值為nil
  • 刪除為子視圖控制器的根視圖配置的約束。
  • 從容器視圖控制器的視圖層中移除該子視圖控制器的根視圖。
  • 調用子視圖控制器的removeFromParentViewController方法來結束父子關系。

刪除子視圖控制器會永久切斷容器視圖控制器與子視圖控制器之間的關系。只有當不再需要引用子視圖控制器時,才能移除子視圖控制器。例如,當一個新視圖控制器推入導航堆棧時,導航控制器并不會移除當前的子視圖控制器。只有當它們從堆棧中彈出時,才會被刪除。

以下代碼顯示了如何從容器視圖控制器中刪除子視圖控制器。調用子視圖控制器的willMoveToParentViewController:方法且傳入nil值為子視圖控制器提供了為更改做準備的機會。子視圖控制器的removeFromParentViewController方法還會調用其didMoveToParentViewController:方法其傳入nil值。將容器視圖控制器設置為nil,也會從容器中刪除子視圖。

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

子視圖控制器之間的轉場動畫

當需要用一個子視圖控制器動畫替換另一個子視圖控制器時,可以將子視圖控制器的添加和刪除合并到轉場動畫過程中。在執行動畫前,請確保兩個子視圖控制器都是容器視圖控制器內容的一部分,但是讓當前的子視圖控制器知道它即將被移除。在動畫過程中,將新子視圖控制器的視圖移動到位并移除舊子視圖控制器的視圖。動畫完成后,移除舊子視圖控制器。

以下代碼顯示了如何使用轉場動畫將一個子視圖控制器替換成另一個子視圖控制器的示例。在這個示例中,transitionFromViewController:toViewController:duration:options:animations:completion:方法會自動更新容器視圖控制器的視圖層次機構,不需要我們自己手動添加和刪除視圖。

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

管理子視圖控制器的外觀更新

在將子視圖控制器添加到容器視圖控制器后,容器視圖控制器會自動將外觀相關的消息轉發給子視圖控制器。大多數情況下,這樣能確保所有事件都正確發生。但是,有時默認行為可能會以無意義的順序發送這些事件。例如,如果多個子視圖控制器同時改變其視圖狀態,則可能需要合并這些更改,以使外觀回調都以更合理的順序同時發生。

要接管外觀回調的責任,需要覆寫容器視圖控制器的shouldAutomaticallyForwardAppearanceMethods方法并返回NO

- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
    return NO;
}

當有轉場動畫發生時,根據需要調用子視圖控制器的beginAppearanceTransition:animated:或者endAppearanceTransition方法。

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

自定義容器視圖控制器的幾點建議

設計、開發和測試新的容器視圖控制器需要時間。雖然每個視圖控制器的行為是直截了當的,但整體控制起來可能相當復雜。在自定義容器控制器時,請考慮以下提示:

  • 只訪問子視圖控制器的根視圖。容器視圖控制器只能訪問每個子視圖控制器的根視圖,也就是子視圖控制器的view屬性,它不應該訪問任何子視圖控制器的其他視圖。
  • 子視圖控制器應該對其容器視圖控制器有最少的了解。子視圖控制器應該關注自己的內容。如果容器視圖控制器允許其行為受到子視圖控制器的影響,則應該使用委托設計模式來管理這些交互。
  • 首選使用常規視圖來設計容器視圖控制器。使用常規視圖(而不是來自子視圖控制器的視圖)使我們有機會在簡單的環境中測試布局約束和轉場動畫。當常規視圖能按照預期工作時,再將常規視圖替換成子視圖控制器的視圖。

將控制委派給子視圖控制器

容器視圖控制器可以將其自身外觀的某些方面委托給其一個或多個子視圖控制器。可以通過以下方式委派控制權:

  • 讓一個子視圖控制器確定狀態欄的樣式。要將狀態欄外觀委托給子視圖控制器,需要覆寫容器視圖控制器的childViewControllerForStatusBarStylechildViewControllerForStatusBarHidden方法中的一個或者兩個。
  • 讓子視圖控制器指定自己的尺寸。具有靈活布局的容器視圖控制器可以使用其子視圖控制器的preferredContentSize屬性來幫助確定子視圖控制器的尺寸。

支持輔助功能

iOS系統為了幫助盲人進行人機交互,設計了VoiceOver讀屏技術。VoiceOver能夠讀出屏幕上的信息,其屬于輔助功能的一部分。有關讓UIViewController支持輔助功能的詳細信息,請參看Supporting Accessibility

保存和恢復狀態

視圖控制器在應用程序狀態保存和恢復過程中起著重要作用。狀態保存會在應用程序被掛起之前保存其當前配置,以便后續應用程序啟動時恢復之前的配置。將應用程序恢復到以前的配置為用戶節省來時間,并提供來更好的用戶體驗。

保存和恢復過程大都是自動的,但是需要我們告知系統應用程序的哪些部分要保存。保存應用程序的視圖控制器的步驟如下:

  • (必需)將恢復標識符分配給要保留其配置的視圖控制器。
  • (必需)告訴系統如何在啟動時創建或定位新的視圖控制器對象。
  • (可選)對于每個視圖控制器,存儲能將視圖控制器返回到其之前配置所需的任何特定配置數據。

有關保存和恢復過程的概述,可以參看App Programming Guide for iOS

標記需要保存的視圖控制器

UIKit只會保存被標記需要保存的視圖控制器。每個視圖控制器都有一個restorationIdentifier屬性,其默認值為nil,為該屬性設置有效的字符串值將告訴UIKit應該保存視圖控制器及其視圖。我們可以以編程方式或在storyboard中為視圖控制器分配恢復標識符。

分配恢復標識符時,一定要記住,當前視圖控制器層次結構中的所有父視圖控制器也必須具有恢復標識符。在保存過程中,UIKit從window的根視圖控制器開始,遍歷當前視圖控制器層次結構。如果該層次結構中的視圖控制器沒有恢復標識符,則視圖控制器及其所有子視圖控制器和呈現的視圖控制器都將被忽略。

選擇有效的恢復標識符

UIKit會使用我們分配的恢復標識符去重新創建視圖控制器,所以需要選擇容易被代碼識別的字符串來作為恢復標識符。如果UIKit無法自動創建一個視圖控制器,其會要求我們自己手動創建,并為我們提供視圖控制器及其所有父視圖控制器的恢復標識符。這個標識符鏈標表示視圖控制器的恢復路徑,以及如何確定正在請求哪個視圖控制器。恢復路徑從根視圖控制器開始,直至所請求的視圖控制器。

恢復標識符通常是視圖控制器的類名。如果在許多地方使用相同的類,則可能需要分配更有意義的值。例如,可以根據視圖控制器管理的數據分配一個字符串。

每個視圖控制器的恢復路徑必須是唯一的。如果容器視圖控制器有兩個子視圖控制器,容器視圖控制器必須為每個子視圖控制器分配一個唯一的恢復標識符。UIKit中的一些容器視圖控制器會自動消除其子視圖控制器的歧義,使得我們可以為每個子視圖控制器使用相同的恢復標識符。例如,UINavigationController類會根據其子視圖控制器在導航堆棧中的位置給其子視圖控制器添加信息。

排除視圖控制器組

要在恢復過程中排除整個視圖控制器組,請將父視圖控制器的恢復標識符設置為nil。下圖顯示了將視圖控制器層次結構中的視圖控制器的恢復標識符設為nil的影響。

圖8-1

排除一個或者多個視圖控制器并不會在隨后的恢復過程中完全移除這些視圖控制器。在應用程序啟動時,任何視圖控制器都是應用程序的默認配置的一部分,它們仍然會被創建,如下圖所示。

圖8-2

在自動保存過程中排除視圖控制器并不會阻止我們去手動保存它。在恢復歸檔中,保存對視圖控制器的引用可以保留視圖控制器及其狀態信息。例如,如果圖7-1中的應用程序委托保存了導航控制器的三個子項,則它們的狀態將被保留。在還原期間,應用程序委托可以重新創建這些視圖控制器并將其推送到導航控制器的堆棧中。

保存視圖控制器的視圖

一些視圖具有與視圖相關的附加狀態信息,例如我們可能想保存滾動視圖的滾動位置。當視圖控制器負責提供滾動視圖的內容時,滾動視圖本身負責保持其視覺狀態。

要保存視圖的狀態,需要執行以下操作:

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

為視圖分配一個恢復標識符將告知UIKit應該將視圖的狀態寫入恢復歸檔,當視圖控制器稍后被恢復時,UIKit還恢復具有恢復標識符的任何視圖的狀態。

在應用程序啟動時恢復視圖控制器

在應用程序啟動時,UIKit會嘗試將應用程序恢復到之前的狀態。此時,UIKit會要求應用程序創建(或定位)包含之前保存的用戶界面的視圖控制器。UIKit在嘗試定位視圖控制器時,會按照以下順序去搜索:

  1. 如果視圖控制器有恢復類,UIkit會要求該類提供視圖控制器。UIKit會調用關聯的恢復類的viewControllerWithRestorationIdentifierPath:coder:方法來檢索視圖控制器。如果該方法返回nil,UIkit會假定應用程序不用重新創建視圖控制器并且會停止查找。
  2. 如果視圖控制器沒有關聯恢復類,UIKit會要求應用程序委托提供視圖控制器。UIKit調用應用程序委托的application:viewControllerWithRestorationIdentifierPath:coder:方法來查找沒有恢復類的視圖控制器。如果該方法返回nil,UIKit將嘗試隱式查找視圖控制器。
  3. 如果具有正確恢復路徑的視圖控制器已經存在,UIKit就會使用該視圖控制器對象。如果應用程序在啟動時創建視圖控制器(以編程方式或者從storyboard中加載),且視圖控制器具有恢復標識符,則UIKit會根據其恢復路徑隱式查找它們。
  4. 如果視圖控制器最初是從storyboarrd中加載的,則UIKit使用保存的storyboard信息來定位和創建它。UIKit將有關視圖控制器的storyboard信息保存在恢復歸檔中,在恢復時,UIKit使用該信息來定位相同的storyboard文件,并且會在使用任何其他方式都沒有查找到視圖控制器的情況下實例化相應的視圖控制器。

為視圖控制器分配一個恢復類可以避免UIKit隱式地檢索該視圖控制器。使用恢復類可以更好地控制是否真的要創建視圖控制器。例如,如果恢復類確定不應該重新創建視圖控制器,則viewControllerWithRestorationIdentifierPath:coder:方法可以返回nil。當沒有恢復類時,UIKit會盡其所能找到或者創建視圖控制器,并將其恢復。

當使用恢復類時,viewControllerWithRestorationIdentifierPath:coder:方法應該創建一個類的新實例對象,執行最低限度的初始化,并返回結果對象。以下代碼展示了一個如何使用此方法從storyboard中加載視圖控制器的例子。由于視圖控制器最初是從storyboard加載的,因此此方法使用UIStateRestorationViewControllerStoryboardKey鍵值從存檔中獲取storyboard。注意,此方法不會嘗試配置視圖控制器的數據內容。當視圖控制器的狀態被解碼后,才會去配置視圖控制器的數據內容。

+ (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數組中的最后一項,并將其分配給視圖控制器。

對于在應用程序啟動時從main storyboard文件中創建的對象,請勿為每個對象創建新的實例。請讓UIKit隱式查找這些對象,或者使用應用程序委托對象的application:viewControllerWithRestorationIdentifierPath:coder:方法來查找現有對象。

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

對于每個要保存的對象,UIKit都會調用對象的encodeRestorableStateWithCoder:方法,使其有機會保存其狀態。在恢復過程中,UIKit會調用對應的decodeRestorableStateWithCoder:方法來解碼該狀態并將其用于對象。對于視圖控制器,這些方法的實現是可選的,但建議實現。可以使用它們來保存和恢復以下類型的信息:

  • 關聯引用所顯示的任何數據(不是數據本身)。
  • 對于容器視圖控制器,關聯引用其子視圖控制器。
  • 與當前選擇有關的信息。
  • 對于具有用戶可配置視圖的視圖控制器,提供關于該視圖當前配置的信息。

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

在實現視圖控制器的encodeRestorableStateWithCoder:decodeRestorableStateWithCoder:方法時,必須調用super的該方法。調用super的該方法讓父類有機會保存和恢復任何附加信息。以下代碼展示來這些方法的一個示例實現,它們保存用于標識指定視圖控制器的數字值。

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

編碼器對象在編碼和解碼過程并不共享。每個具有可保存狀態的對象都會接收到自己的編碼器對象。使用各自的編碼器意味著我們不用擔心密鑰之間的命名空間沖突。但是,請不要使用UIApplicationStateRestorationBundleVersionKeyUIApplicationStateRestorationUserInterfaceIdiomKeyUIStateRestorationViewControllerStoryboardKey鍵名稱。UIKit使用這些鍵來存儲關于視圖控制器狀態的附加信息。

有關實現視圖控制器的編碼和解碼的更多信息,請參看UIViewController Class Reference

保存和恢復視圖控制器的幾點建議

在視圖控制器中添加對狀態保存和恢復的支持時,請考慮以下準則:

  • 請記住,我們可能不想保留所有視圖控制器。在某些情況下,保留視圖控制器可能沒有意義。例如,如果應用程序正在顯示更改,則可能需要取消操作并將應用程序還原到上一個屏幕。在這種情況下,我們不需要保留要求輸入新密碼信息的視圖控制器。
  • 避免在恢復過程中換掉視圖控制器對象所屬的類。狀態保存系統對它保存的視圖控制器的類進行進行編碼。在恢復過程中,如果應用程序返回的對象的類不匹配(或者不是原始對象的子類),則系統不會要求視圖控制器解碼任何狀態信息。
  • 狀態保存系統希望我們按照預期使用視圖控制器。恢復過程依賴于視圖控制器的包含關系來重建界面。如果我們沒有正確使用容器視圖控制器,保存系統將找不到視圖控制器。例如,除非在相應的視圖控制器之間存在包含關系,否則不要將視圖控制器的視圖嵌入到不同的視圖中。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容