iOS UI狀態保存和恢復

iOS 開發中,我們都知道一個App點擊了home按鍵或者切換至其他應用時,將進入後臺。隨着時間的推移,App會經歷後臺運行,後臺懸掛,最後被殺死。假如有這樣一個場景:

場景1:用戶正在使用我們App進行個人信息的編輯,突然接到了一個電話,使得App進入後臺並且通話時間超過了App後臺保活的時間。當用戶通話完畢的時候,返回繼續填寫,卻發現App重新啓動了,並且用戶之前填寫的數據,都沒有保存,需要重新輸入?用戶的體驗會很不好。

對於此問題,我們可能會說讓App後臺保持活躍不就行啦。是的,這是個很好的解決方案。但是除了這個方案,我們是不是有其他的辦法實現UI界面和數據的保存和恢復。答案是肯定的,接下來我們會介紹一種方案UIStateRestoration。

一、關於UIStateRestoration

UIStateRestoration出現於iOS 6.0以後的API中。主要幫助我們實現特定場景下的UI保存和恢復。UIStateRestoration是一個協議類,在蘋果的系統中UIKit框架下的UIApplication、UIViewController、UIView都實現了UIStateRestoration協議。

關於UI狀態從應用程序啓動到恢復以及UI狀態保存時相關API的調用順序,用官網的圖解大家可以理解的更清楚。



二、UIStateRestoration的介紹

  1. 系統進行UI狀態的保存和恢復時,自動使用以下常量字符串,進行相關數據的歸檔。
#pragma mark -- State Restoration Coder Keys --
// UIStoryBoard that originally created the ViewController that saved state, nil if no UIStoryboard
//保存和創建一個故事版用到的key
UIKIT_EXTERN NSString *const UIStateRestorationViewControllerStoryboardKey NS_AVAILABLE_IOS(6_0);
// NSString with value of info.plist's Bundle Version (app version) when state was last saved for the app
//應用程序上次狀態保存時info.plist的應用程序版本
UIKIT_EXTERN NSString *const UIApplicationStateRestorationBundleVersionKey NS_AVAILABLE_IOS(6_0);
// NSNumber containing the UIUserInterfaceIdiom enum value of the app that saved state
//狀態保存時應用程序的`UIUserInterfaceIdiom`枚舉值
UIKIT_EXTERN NSString *const UIApplicationStateRestorationUserInterfaceIdiomKey NS_AVAILABLE_IOS(6_0);
// NSDate specifying the date/time the state restoration archive was saved. This is in UTC.
//狀態保存的時間,UTC格式。
UIKIT_EXTERN NSString *const UIApplicationStateRestorationTimestampKey NS_AVAILABLE_IOS(7_0);
// NSString with value of the system version (iOS version) when state was last saved for the app
//上次應用程序保存狀態時的系統版本(iOS版本)
UIKIT_EXTERN NSString *const UIApplicationStateRestorationSystemVersionKey NS_AVAILABLE_IOS(7_0);
  1. UIViewControllerRestoration協議:在UI狀態恢復時幫我們生成一個控制器。
#pragma mark -- State Restoration protocols for UIView and UIViewController --
// A class must implement this protocol if it is specified as the restoration class of a UIViewController.
//如果將類指定爲UIViewController的恢復類,則必須實現此協議。
@protocol UIViewControllerRestoration
+ (nullable UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder;
@end
  1. UIDataSourceModelAssociation協議:目前只有UITableView and UICollectionView實現了這個協議。
    官網說明: UIDataSourceModelAssociation.
@protocol UIDataSourceModelAssociation
- (nullable NSString *) modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view;
- (nullable NSIndexPath *) indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view;
@end
  1. UIStateRestoring協議:實現UIStateRestoring協議,可以讓我們自定義的視圖(非UIView和UIViewController子類)加入狀態恢復。前提必須使用UIApplication的+ (void)registerObjectForStateRestoration:(id<UIStateRestoring>)object restorationIdentifier:(NSString *)restorationIdentifier方法註冊。
+ (void)registerObjectForStateRestoration:(id<UIStateRestoring>)object restorationIdentifier:(NSString *)restorationIdentifier
@protocol UIObjectRestoration;
// Conform to this protocol if you want your objects to participate in state restoration. 
// To participate in state restoration, the function registerObjectForStateRestoration must
// be called for the object.
/*如果您希望對象參與狀態恢復,請遵守此協議。
要參與狀態恢復,函數registerObjectForStateRestoration必須爲此對象而調用。*/
@protocol UIStateRestoring <NSObject>
@optional
// The parent property is used to scope the restoration identifier path for an object, to
// disambiguate it from other objects that might be using the same identifier. The parent
// must be a restorable object or a view controller, else it will be ignored.
/*parent屬性用於定義一個對象的恢復標識恢復路徑,以便從可能使用相同恢復標識的其他對象中消除歧義。
parent屬性必須是可恢復對象`id<UIStateRestoring> `或視圖控制器,否則將被忽略。
個人理解:類似繼承體系模式,方便歸整清楚恢復的路徑,幫助我們進行一定順序和層次的恢復。*/
@property (nonatomic, readonly, nullable) id<UIStateRestoring> restorationParent;
// The restoration class specifies a class which is consulted during restoration to find/create
// the object, rather than trying to look it up implicitly
/*
objectRestorationClass指定在恢復期間用於查找和創建需要恢復的對象的類。
並不是試圖隱式查找和創建需要恢復的對象
*/
@property (nonatomic, readonly, nullable) Class<UIObjectRestoration> objectRestorationClass;
// Methods to save and restore state for the object. If these aren't implemented, the object
// can still be referenced by other objects in state restoration archives, but it won't
// save/restore any state of its own.
/*
保存和恢復對象狀態的方法。
如果沒有實現這些方法,對象仍可以被狀態恢復歸檔中的其他對象引用,但它將不會保存和恢復自己的任何狀態。
*/
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder;
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder;
// applicationFinishedRestoringState is called on all restored objects that implement the method *after* all other object
// decoding has been done (including the application delegate). This allows an object to complete setup after state
// restoration, knowing that all objects from the restoration archive have decoded their state.
/*在所有其他對象實現恢復方法,解碼完成(包括`AppDelegate`的解碼)並恢復了所有的可恢復對象後纔會調用applicationFinishedRestoringState。
這允許對象在狀態恢復之後完成設置,可以通過此方法明確知道恢復檔案中的所有對象都已解碼其狀態
*/
- (void) applicationFinishedRestoringState;
@end
// Protocol for classes that act as a factory to find a restorable object during state restoration
// A class must implement this protocol if it is specified as the restoration class of a UIRestorableObject.
//作爲工廠類的協議,用於在狀態恢復期間查找可恢復對象。如果指定某個類爲`id<UIStateRestoring>`的`objectRestorationClass `,則該類必須實現此協議。
@protocol UIObjectRestoration
+ (nullable id<UIStateRestoring>) objectWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder;
@end

UIStateRestoration場景

適用於App進入後臺,後臺停留時間超過系統分配的後臺活躍時間後被系統殺死時的場景。因爲當用戶強制退出應用程序時,系統會自動刪除應用程序的保留狀態。在應用程序被終止時刪除保留的狀態信息是一項安全預防措施。如果應用程序在啓動時崩潰,系統也會刪除保留狀態作爲類似的安全預防措施。

UIStateRestoration調試

根據場景描述,如果要測試應用程序恢復其狀態的能力,則在調試期間不應使用多任務欄來強制終止應用程序。可以通過設置項目的plist文件下Application does not run in background爲YES。

UIApplication對於UIStateRestoration協議的實現接口

#pragma mark -- State Restoration protocol adopted by UIApplication delegate --
- (nullable UIViewController *) application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (BOOL) application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (BOOL) application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (void) application:(UIApplication *)application willEncodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (void) application:(UIApplication *)application didDecodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);

UIViewController對於UIStateRestoration協議的實現接口

@interface UIViewController (UIStateRestoration) <UIStateRestoring>
@property (nullable, nonatomic, copy) NSString *restorationIdentifier NS_AVAILABLE_IOS(6_0);
@property (nullable, nonatomic, readwrite, assign) Class<UIViewControllerRestoration> restorationClass NS_AVAILABLE_IOS(6_0);
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (void) applicationFinishedRestoringState NS_AVAILABLE_IOS(7_0);
@end

UIView對於UIStateRestoration協議的實現接口

@interface UIView (UIStateRestoration)
@property (nullable, nonatomic, copy) NSString *restorationIdentifier NS_AVAILABLE_IOS(6_0);
- (void) encodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
- (void) decodeRestorableStateWithCoder:(NSCoder *)coder NS_AVAILABLE_IOS(6_0);
@end

上面篇幅講了UI狀態保存和恢復的流程,UIStateRestoration協議類的方法,適用場景,調試策略以及UIApplication、UIViewController、UIView關於UIStateRestoration協議所提供的接口方法。

在AppDelegate.m中設置UI的狀態可以恢復和保存

- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder {
    return YES;
}
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder {
    return YES;
}

相應的UIViewController中重寫以下方法

//進入後臺時調用;使用此方法保存我們需要下次恢復的數據。
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder; {
    [super encodeRestorableStateWithCoder:coder];
   //保存數據的代碼寫在這裏
  [coder encodeObject: _nameTextField.text ?: @"" forKey:nameKey];
    
}
//進入前臺時調用;使用此方法恢復數據,並展示。
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder; {
    [super decodeRestorableStateWithCoder:coder];
    self.name =  [NSString stringWithString:[coder decodeObjectForKey:nameKey]];
    _nameTextField.text = self.name;
}

設置完這兩項,真的就可以了嗎?我們可能會發現新建一個工程,直接使用自帶的ViewController打個斷點,發現成功調用UIViewController中重寫的encodeRestorableStateWithCoderdecodeRestorableStateWithCoder方法,進行了數據的保存。但是使用UINavigationController 或者UITabBarController進行多層嵌套後,以上方法卻沒有被調用。其實這一切只是因爲Xcode給我配置的初始項目中,ViewController是主window的根控制器,不存在UITabBarControllerUINavigationController的嵌套,界面展示的控制器顯示單一,也不會存在多層,並且此ViewController還是直接從故事版實例化的。

場景2:
主window的根控制器爲以ViewController A 初始化的一個UINavigationController,在ViewController A中有一個按鈕點擊跳轉進入ViewController B,此時使用調試方法,讓程序退出。再次啓動UI狀態是否恢復到ViewController B。

按照場景2,我們需要恢復到ViewController B,若不管中間的控制器ViewController ANavigationController便會斷層,顯示這不是我們想要的;所以我們需要在應用重啓時,不僅還原ViewController B,還希望ViewController A按照層級還原,如若ViewController A中還有要恢復的數據,也一併恢復。

嵌套控制器設置

逐層設置restorationIdentifier,並重寫相應的保存與恢復方法
  1. storyboard實例化的控制器設置恢復標識


  2. 代碼設置恢復標識

self.restorationIdentifier = NSStringFromClass(self.class);

注意:

所有通向ViewController B的視圖控制器必須具有還原標識符(包括初始的UINavigationControllerUITabBarController),否則狀態還原將無法工作。即:需要設置restorationIdentifier

嵌套控制器的恢復

方案一
  1. 設置ViewController中定義的restorationClass屬性。
 //! 設置恢復標識
self.restorationIdentifier = NSStringFromClass(self.class);
//! 設置用於恢復的類
self.restorationClass = self.class;

restorationClass:Class的實例對象,APP狀態恢復的時候負責重新創建當前的控制器 ,需要實現定義在UIStateRestoring.h中的UIViewControllerRestoration協議。restorationClass可以是當前控制器也可以是其他對象,只要實現了UIViewControllerRestoration協議即可。

  1. 在指定的restorationClass中恢復當前控制器。
+ (nullable UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder {
    //! identifierComponents返回的就是我們之前設置的restorationIdentifier
    PersonDetailController *ctrl = [[PersonDetailController alloc]init];
    ctrl.restorationIdentifier = identifierComponents.lastObject;
    ctrl.restorationClass = [self class];
    return ctrl;
}

總結:多層控制器,每層控制器都需要在所屬的類中設置restorationClass同時必須實現UIViewControllerRestoration方法,兩者缺一不可。

方案二

多層級嵌套時,每個控制器中不需要單獨設置restorationClass,或者每個控制都沒有指定restorationClass時。則需要實現UIApplication對於UIStateRestoration協議所實現接口方法,讓我們可以在恢復期間創建每個層級的控制器。

- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder {
   
    UIViewController *vc;
    UIStoryboard *storyboard = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
    if (storyboard){
        return nil;
    } else {
      vc = [[NSClassFromString(identifierComponents.lastObject) alloc]init];
    }

    return vc;
}

上述代碼中,爲什麼從storyboard恢復的部分,就直接返回nil了呢?爲什麼不使用如下方式把控制器實例化完成呢?:

vc = [storyboard instantiateViewControllerWithIdentifier:identifierComponents.lastObject];
vc.restorationIdentifier = [identifierComponents lastObject];
vc.restorationClass = NSClassFromString(identifierComponents.lastObject);

在筆者的親測過程中發現這樣做會多實例化一次vc對象,會影響vc界面恢復的數據展示。這是因爲來自storyboard的視圖,會由UIKIT 自動幫我們查找和創建視圖控制器。
總結:多層控制器統一在AppDelegate中實現各個層級控制器的恢復,比較方便。

注意:

1.通向ViewController B的視圖控制器若實現restorationClass和UIViewControllerRestoration組合後,則不會調用UIApplication對於UIStateRestoration協議所實現接口方法,否則恢復時回調用。
2.如果我們沒有指明,恢復每一個控制器時 用於創建此控制器的對象所屬的類,則必須在AppDelegate中實現此方法,讓我們可以在恢復期間創建一個新的控制器。
3.來自故事版的視圖,恢復時會由UIKIT 自動幫我們查找和創建視圖控制器。

前面我們介紹了UI狀態保存和恢復的流程,UIStateRestoration協議類的方法,適用場景,調試策略,UIApplication,UIViewController,UIView關於UIStateRestoration協議所提供的接口方法以及如何實現UI狀態保存和恢復。接下來我們將介紹UIStateRestoration協議類中的UIDataSourceModelAssociation協議。

關於UIDataSourceModelAssociation協議

引用官網的解釋

Your data source objects can adopt this protocol to assist a corresponding table or collection view during the state restoration process. Those classes use the methods of this protocol to ensure that the same data objects (and not just the same row indexes) are scrolled into view and selected.
//你的數據源對象可以實現這個協議,在狀態恢復的過程中去支持相關的table or collection view;這些實現了該協議的類,使用這個協議的方法去保證相同的數據對象,(而不僅僅是相同的行的索引)被滾動到視圖並且被選中。
Before you can implement this protocol, your app must be able to identify data objects consistently between app launches. This requires being able to take some identifying marker of the object and convert that marker into a string that can then be saved with the rest of the app state. For example, a Core Data app could convert a managed object’s ID into a URI that it could then convert into a string.
//在你實現這個協議之前,你的App必須能夠在App啓動之間,一直(總是可以)辨別出數據源對象。這就要求對象能夠有一些辨認標識,並且可以把標識轉換爲當App狀態不活躍時能夠被存儲的字符串;
Currently, only the UITableView and UICollectionView classes support this protocol. You would implement this protocol in any objects you use as the data source for those classes. If you do not adopt the protocol in your data source, the views do not attempt to restore the selected and visible rows.
//目前,只有 UITableView 和 UICollectionView 類 支持這個協議。你將可以實現這個協議在任何你用來作爲UITableView 和 UICollectionView數據源的對象中,如果在你的數據源對象中不實現這個協議,那麼視圖將不會試着去恢復選中的和可見rows;

我們可以獲取到的主要信息有:

  • 只有 UITableView 和 UICollectionView類支持這個協議。

  • 我們的數據源中的每個數據對象(model)必須具備唯一辨認標識。

  • 使用這個協議的方法去保證相同的數據對象,(而不僅僅是相同的行的索引)被滾動到視圖並且被選中。舉個場景的例子:TableView的數據源對象在上次保存時,所保存的行的索引,可能會因爲在當前運行週期內數據源中數據的變動發生變化。從而導致當前選中的行所對應的數據並非上次保存時的數據。

  • 若需要使用UIDataSourceModelAssociation,則:實現了UITableView 和 UICollectionView數據源協議的對象,負責實現這個協議的方法,否則不會生效。實際操作發現確實如此。

除了官網解釋,在實際操作中發現還需要設置UITableViewUICollectionViewrestorationIdentifier,否則UIDataSourceModelAssociation協議方法不會被調用。關於UITableView的restorationIdentifier查閱官方文檔如下:

To save and restore the table’s data, assign a nonempty value to the table view’s restorationIdentifier property. When its parent view controller is saved, the table view automatically saves the index paths for the currently selected and visible rows. If the table’s data source object adopts the UIDataSourceModelAssociation protocol, the table stores the unique IDs that you provide for those items instead of their index paths.

UITableView設置了restorationIdentifier,進行UI的保存時,tableView會自動存儲當前選中和可見行的索引。補充:還會存儲滾動偏移,並可以恢復。

UIDataSourceModelAssociation使用

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    InfoModel *model = [self.dataSource objectAtIndex:indexPath.row];
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(UITableViewCell.class) forIndexPath:indexPath];
    cell.textLabel.text = model.title;
    cell.restorationIdentifier = model.identifier;
    
    return cell;
}
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view {
    //根據index 返回identifier
    NSString *identifier = nil;
    InfoModel *model = [self.dataSource objectAtIndex:idx.row];
    
    /*
     註釋①
     if (idx && view) {
       identifier = model.identifier;
    }
    */
    if (idx.row == _currentPath.row && view) {
        identifier = model.identifier;
    }
    //若是不定義_currentPath追蹤當前選中的cell.會多保存一個cell,目前尚未有答案。
    return identifier;
}
//此方法 恢復時調用
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view {
    //根據identifier 返回index;
    NSIndexPath *indexPath = nil;
    if (identifier && view) {
        __block NSInteger row = 0;
        [self.dataSource enumerateObjectsUsingBlock:^(InfoModel *obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([obj.identifier isEqualToString:identifier]) {
                row = idx;
                *stop = YES;
            }
        }];
        indexPath = [NSIndexPath indexPathForRow:row inSection:0];
        _currentPath = indexPath;
        NSLog(@"當前選中的數據源對象標識是:%@,對象擡頭是:%@",[self.dataSource[indexPath.row] identifier],[self.dataSource[indexPath.row] title]);
    }

    return indexPath;
}

上述代碼方法-(NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view中註釋①描述:此方法會在保存時調用兩次,idx所返回的數據除了我們選中的行,還會返回一個其他行。 若是採用這種方式映射唯一標識,會出現保存了我們不需要的行的標識,導致恢復滑動位置失效,針對此問題目前筆者尚未有答案,查閱資料發現這個問題曾經是蘋果的一個BUG,若是大家知道具體原因,歡迎評論和補充。目前在此基礎上筆者自己想的解決辦法:定義_currentPath追蹤當前選中的cell,保存時根據_currentPath保存我們需要的標識,測試中發現可以解決問題。

QIRestorationDemo地址

[上一篇]:iOS/macOC info.plist權限配置
[下一篇]:iOS UITextField輸入身份證號設置






我的專題:

iOS開發

Mac漢化(遊戲/軟件)

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