iOS UI状态保存和恢复

作者: Apple_JinlongLu | 来源:发表于2019-08-22 11:53 被阅读0次

    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的调用顺序,用官网的图解大家可以理解的更清楚。


    UI状态从应用程序启动到恢复调用顺序说明
    UI状态保存时调用顺序说明

    二、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实例化的控制器设置恢复标识


      storyboard设置restorationIdentifier
    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汉化(游戏/软件)

    相关文章

      网友评论

        本文标题:iOS UI状态保存和恢复

        本文链接:https://www.haomeiwen.com/subject/mlzgsctx.html