前言
最近项目需求加上状态恢复, 记得之前在书上看过, 这次单独抽出这个功能实现详细梳理一下, 方便自己温习一下, 也方便不知道的 developer 学习.
状态恢复?
举个栗子:
在使用名字为 A 的 app 时, 从列表页面进入详情页面, 这时你不想看了, 点击 Home 键, 回到后台, 打开 B 开始玩. 过了一段时间之后, 由于 A 没有写后台运行的功能, 这时, 系统会关闭 A, 再打开时, 你看到的是之前进入的详情页面.
系统一点的话说就是, 系统在进入后台时会保存 app 的层次结构, 在下一次进入的时候会恢复这个结构中所有的 controller. 系统在终止之前会遍历结构中每一个节点, 恢复标识, 类, 保存的数据. 在终止应用之后, 系统会把这些信息存储在系统文件中.
恢复标识
一般和对象的类名相同, 其类被称为恢复类.
##实现
下面通过一个 demo 演示状态恢复的实现, 这个 demo 是一个保存联系人信息的 demo. 以下代码以 demo 中控制器为例. 建议 demo 和本文一起看, 更好理解.
###1. 开启
默认情况下, app 的状态恢复是关闭的, 需要我们手动开启.
在 AppDelegate.m 中手动打开:
1 |
|
系统在保存 app 状态时, 会先从 root VC 去查询是否有restorationIdentifier
属性, 如果有, 则保存状态, 继续查询其子控制器, 有则保存. 直到找不到带有restorationIdentifier
的子控制器, 系统会停止保存其与其子控制器的状态.
画个图解释一下:
上图三级 VC 即使有restorationIdentifier
也不会恢复.
application:willFinishLaunchingWithOptions:
方法会在启用状态恢复之前调用, 我们需要将触发启用方法之前的代码写在这个方法中.
1 | - (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
然后为根视图控制器添加恢复标识:
1 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { |
2. 为子控制器实现
a. 设置恢复标识和恢复类
在一级控制器初始化方法中为其设置:
1 |
|
在子控制器中设置:
1 | - (instancetype)initWithNewItem:(BOOL)isNew { |
如果是模态推出带有navigationController
的控制器, 需要为这个 nav 设置恢复标识:
1 | - (void)addNewItem:(id)sender { |
b. 遵循恢复协议
需要状态恢复的控制器需要遵循<UIViewControllerRestoration>
协议:
一级视图控制器中:
1 |
|
同样, 二级视图控制器中, demo 中添加新联系人信息和查看联系人信息调用的是同一个控制器, 初始化方法为自己封装的方法newItem:(BOOL)isNew
, isNew 为 NO 时, 查看联系人, 为 YES 时, 新建联系人. 此时有两种情况:
新建联系人:
在恢复状态时
newItem:(BOOL)isNew
参数传入 YES查看联系人:
参数传入 NO
那么如何判断传入什么参数呢? 通过
1 | + (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder; |
方法中的identifierComponents
来判断, identifierComponents
存储了当前视图控制器及其所有上级视图控制器的恢复标识. 那么现在我们来看一下:
- 新建联系人程序中的恢复标识有:
- root VC 的 nav 恢复标识
- 二级 VC 的 nav 恢复标识(没有一级 VC 的标识是因为 二级 VC 是由一级 VC 的 nav 模态出来的)
- 二级 VC 自身的恢复标识
- 查看联系人的恢复标识有:
- 根 VC 的 nav 恢复标识
- 二级 VC 自身的恢复标识(没有一级的和上面同理)
所以新建联系人的 VC 的identifierComponents
的个数为3, 查看联系人的为2个. 那么则可以判断参数如何传递:
1 |
|
c. 为 nav 设置恢复类
1 | // 如果某个对象没有设置恢复类, 那么系统会通过 AppDelegate 来创建 |
至此, 控制器的状态恢复已完成, 但是现实的数据还需要做持久化处理, 否则只是恢复了一个没有数据的控制器.
d. 数据持久化
使二级页面详情页需要的数据保存:
1 | - (void)encodeRestorableStateWithCoder:(NSCoder *)coder { |
二级页面状态恢复完成, 这时候测试(测试方法: 运行后, cmd + shift + h回到桌面, Xcode停止运行, 然后再运行), 重新打开项目, 发现视图控制器状态是恢复了, 但是数据还是空白. 然后打上断点看了下周期, 把数据获取方法写在viewWillAppear:
里就好了.
e. 记录 tableview 状态
为一级 VC 设置其 tableView 的恢复标识:
1 | - (void)viewDidLoad { |
记录 tableView 是否处于 editing 状态:
1 | // 记录 tableView 是否处于编辑状态 |
通过<UIDataSourceModelAssociation>
协议使视图对象在恢复时关联正确的 model 对象. 当保存状态时, 其会根据 indexPath 保存一个唯一标识.
实现<UIDataSourceModelAssociation>
协议方法:
1 | - (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view { |
最后记得在进入后台前持久化当前的 item(实际开发中记得用 cache(项目里使用 YYCache) 或者 db(项目里使用 FMDB) 去即时持久化视图数据, 是一个比较稳妥的方案):
1 | - (void)applicationDidEnterBackground:(UIApplication *)application { |
至此, 状态恢复基本使用已经实现.
测试
- 添加 n 个新的联系人, 滑动列表到测试位置, 让 tableView 进入到编辑状态. 按下
cmd + shift + h
进入 home, 用 Xcode 结束程序cmd+.
, 再次运行看看是否在最后滑动位置, 或者是否处于编辑状态. - 恢复编辑状态, 随便进入一个联系人详情, 重复上面的操作, 看看进入程序之后是否处于上次退出前的详情页面.