前言
最近項(xiàng)目需求加上狀態(tài)恢復(fù), 記得之前在書(shū)上看過(guò), 這次單獨(dú)抽出這個(gè)功能實(shí)現(xiàn)詳細(xì)梳理一下, 方便自己溫習(xí)一下, 也方便不知道的 developer 學(xué)習(xí).
狀態(tài)恢復(fù)?
舉個(gè)栗子:
在使用名字為 A 的 app 時(shí), 從列表頁(yè)面進(jìn)入詳情頁(yè)面, 這時(shí)你不想看了, 點(diǎn)擊 Home 鍵, 回到后臺(tái), 打開(kāi) B 開(kāi)始玩. 過(guò)了一段時(shí)間之后, 由于 A 沒(méi)有寫(xiě)后臺(tái)運(yùn)行的功能, 這時(shí), 系統(tǒng)會(huì)關(guān)閉 A, 再打開(kāi)時(shí), 你看到的是之前進(jìn)入的詳情頁(yè)面.
系統(tǒng)一點(diǎn)的話說(shuō)就是, 系統(tǒng)在進(jìn)入后臺(tái)時(shí)會(huì)保存 app 的層次結(jié)構(gòu), 在下一次進(jìn)入的時(shí)候會(huì)恢復(fù)這個(gè)結(jié)構(gòu)中所有的 controller. 系統(tǒng)在終止之前會(huì)遍歷結(jié)構(gòu)中每一個(gè)節(jié)點(diǎn), 恢復(fù)標(biāo)識(shí), 類, 保存的數(shù)據(jù). 在終止應(yīng)用之后, 系統(tǒng)會(huì)把這些信息存儲(chǔ)在系統(tǒng)文件中.
恢復(fù)標(biāo)識(shí)
一般和對(duì)象的類名相同, 其類被稱為恢復(fù)類.
實(shí)現(xiàn)
下面通過(guò)一個(gè) demo 演示狀態(tài)恢復(fù)的實(shí)現(xiàn), 這個(gè) demo 是一個(gè)保存聯(lián)系人信息的 demo. 以下代碼以 demo 中控制器為例. 建議 demo 和本文一起看, 更好理解.
1. 開(kāi)啟
默認(rèn)情況下, app 的狀態(tài)恢復(fù)是關(guān)閉的, 需要我們手動(dòng)開(kāi)啟.
在 AppDelegate.m 中手動(dòng)打開(kāi):
#pragma mark - open state restoration
// 和NSCoding協(xié)議方法有點(diǎn)像, encode, decode
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder {
return YES;
}
- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder {
return YES;
}
系統(tǒng)在保存 app 狀態(tài)時(shí), 會(huì)先從 root VC 去查詢是否有restorationIdentifier屬性, 如果有, 則保存狀態(tài), 繼續(xù)查詢其子控制器, 有則保存. 直到找不到帶有restorationIdentifier的子控制器, 系統(tǒng)會(huì)停止保存其與其子控制器的狀態(tài).
畫(huà)個(gè)圖解釋一下:

上圖三級(jí) VC 即使有restorationIdentifier也不會(huì)恢復(fù).
application:willFinishLaunchingWithOptions:方法會(huì)在啟用狀態(tài)恢復(fù)之前調(diào)用, 我們需要將觸發(fā)啟用方法之前的代碼寫(xiě)在這個(gè)方法中.
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] init];
self.window.frame = [UIScreen mainScreen].bounds;
self.window.backgroundColor = [UIColor whiteColor];
return YES;
}
然后為根視圖控制器添加恢復(fù)標(biāo)識(shí):
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 如果沒(méi)有觸發(fā)恢復(fù), 則重新設(shè)置根控制器
if (!self.window.rootViewController) {
YNMainTableController *table = [[YNMainTableController alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:table];
nav.restorationIdentifier = NSStringFromClass([nav class]);
self.window.rootViewController = nav;
}
[self.window makeKeyAndVisible];
return YES;
}
2. 為子控制器實(shí)現(xiàn)
a. 設(shè)置恢復(fù)標(biāo)識(shí)和恢復(fù)類
在一級(jí)控制器初始化方法中為其設(shè)置:
#pragma mark - initial
- (instancetype)init {
self = [super init];
if (self) {
// 設(shè)置恢復(fù)標(biāo)識(shí)和恢復(fù)類
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [self class];
}
return self;
}
在子控制器中設(shè)置:
- (instancetype)initWithNewItem:(BOOL)isNew {
self = [super initWithNibName:nil bundle:nil];
if (self) {
// 設(shè)置恢復(fù)類和恢復(fù)標(biāo)識(shí)
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [self class];
if (isNew) {
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(save:)];
self.navigationItem.rightBarButtonItem = doneItem;
UIBarButtonItem *cancelItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)];
self.navigationItem.leftBarButtonItem = cancelItem;
}
}
return self;
}
如果是模態(tài)推出帶有navigationController的控制器, 需要為這個(gè) nav 設(shè)置恢復(fù)標(biāo)識(shí):
- (void)addNewItem:(id)sender {
YNCustomItem *item = [[YNItemHandler sharedStore] createItem];
YNSonViewController *sonVC = [YNSonViewController newItem:YES];
sonVC.item = item;
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:sonVC];
// 為 UINavigationController 設(shè)置恢復(fù)類
nav.restorationIdentifier = NSStringFromClass([nav class]);
[self presentViewController:nav animated:YES completion:nil];
}
b. 遵循恢復(fù)協(xié)議
需要狀態(tài)恢復(fù)的控制器需要遵循<UIViewControllerRestoration>協(xié)議:
一級(jí)視圖控制器中:
#pragma mark - view controller restoration
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
return [[self alloc] init];
}
同樣, 二級(jí)視圖控制器中, demo 中添加新聯(lián)系人信息和查看聯(lián)系人信息調(diào)用的是同一個(gè)控制器, 初始化方法為自己封裝的方法newItem:(BOOL)isNew, isNew 為 NO 時(shí), 查看聯(lián)系人, 為 YES 時(shí), 新建聯(lián)系人. 此時(shí)有兩種情況:
-
新建聯(lián)系人:
在恢復(fù)狀態(tài)時(shí)
newItem:(BOOL)isNew參數(shù)傳入 YES -
查看聯(lián)系人:
參數(shù)傳入 NO
那么如何判斷傳入什么參數(shù)呢? 通過(guò)
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder;
方法中的identifierComponents來(lái)判斷, identifierComponents存儲(chǔ)了當(dāng)前視圖控制器及其所有上級(jí)視圖控制器的恢復(fù)標(biāo)識(shí). 那么現(xiàn)在我們來(lái)看一下:
- 新建聯(lián)系人程序中的恢復(fù)標(biāo)識(shí)有:
- root VC 的 nav 恢復(fù)標(biāo)識(shí)
- 二級(jí) VC 的 nav 恢復(fù)標(biāo)識(shí)(沒(méi)有一級(jí) VC 的標(biāo)識(shí)是因?yàn)?二級(jí) VC 是由一級(jí) VC 的 nav 模態(tài)出來(lái)的)
- 二級(jí) VC 自身的恢復(fù)標(biāo)識(shí)
- 查看聯(lián)系人的恢復(fù)標(biāo)識(shí)有:
- 根 VC 的 nav 恢復(fù)標(biāo)識(shí)
- 二級(jí) VC 自身的恢復(fù)標(biāo)識(shí)(沒(méi)有一級(jí)的和上面同理)
所以新建聯(lián)系人的 VC 的identifierComponents的個(gè)數(shù)為3, 查看聯(lián)系人的為2個(gè). 那么則可以判斷參數(shù)如何傳遞:
#pragma mark - view controller restoration
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
BOOL isNew = NO;
if (identifierComponents.count == 3) {
isNew = YES;
}
return [[self alloc] initWithNewItem:isNew];
}
c. 為 nav 設(shè)置恢復(fù)類
// 如果某個(gè)對(duì)象沒(méi)有設(shè)置恢復(fù)類, 那么系統(tǒng)會(huì)通過(guò) AppDelegate 來(lái)創(chuàng)建
- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
UINavigationController *nav = [[UINavigationController alloc] init];
// 恢復(fù)標(biāo)識(shí)路徑中最后一個(gè)對(duì)象就是 nav 的恢復(fù)標(biāo)識(shí)
nav.restorationIdentifier = [identifierComponents lastObject];
if (identifierComponents.count == 1) {
self.window.rootViewController = nav;
}
return nav;
}
至此, 控制器的狀態(tài)恢復(fù)已完成, 但是現(xiàn)實(shí)的數(shù)據(jù)還需要做持久化處理, 否則只是恢復(fù)了一個(gè)沒(méi)有數(shù)據(jù)的控制器.
d. 數(shù)據(jù)持久化
使二級(jí)頁(yè)面詳情頁(yè)需要的數(shù)據(jù)保存:
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[coder encodeObject:self.item.itemKey forKey:kRestorationKey];
// 保存 textField 中的文本, 以便恢復(fù)更改后的文本
self.item.name = self.nameField.text;
self.item.phoneNumber = [self.phoneField.text integerValue];
self.item.sex = self.sexField.text;
// 存入本地
[[YNItemHandler sharedStore] saveItems];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
NSString *itemKey = [coder decodeObjectForKey:kRestorationKey];
for (YNCustomItem *item in [[YNItemHandler sharedStore] allItems]) {
if ([item.itemKey isEqualToString:itemKey]) {
self.item = item;
NSLog(@"name:%@, phone:%ld, sex:%@", self.item.name, self.item.phoneNumber, self.item.sex);
break;
}
}
[super decodeRestorableStateWithCoder:coder];
}
二級(jí)頁(yè)面狀態(tài)恢復(fù)完成, 這時(shí)候測(cè)試(測(cè)試方法: 運(yùn)行后, cmd + shift + h回到桌面, Xcode停止運(yùn)行, 然后再運(yùn)行), 重新打開(kāi)項(xiàng)目, 發(fā)現(xiàn)視圖控制器狀態(tài)是恢復(fù)了, 但是數(shù)據(jù)還是空白. 然后打上斷點(diǎn)看了下周期, 把數(shù)據(jù)獲取方法寫(xiě)在viewWillAppear:里就好了.
e. 記錄 tableview 狀態(tài)
為一級(jí) VC 設(shè)置其 tableView 的恢復(fù)標(biāo)識(shí):
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"State Restoration";
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addNewItem:)];
[self.tableView registerNib:[UINib nibWithNibName:NSStringFromClass([YNTableViewCell class]) bundle:nil] forCellReuseIdentifier:kCellIdentifier];
// 給 tableView 設(shè)置恢復(fù)標(biāo)識(shí), tableView 自動(dòng)保存的 contentOffset 會(huì)恢復(fù)其滾動(dòng)位置
self.tableView.restorationIdentifier = kTableViewIdentifier;
}
記錄 tableView 是否處于 editing 狀態(tài):
// 記錄 tableView 是否處于編輯狀態(tài)
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[coder encodeBool:self.isEditing forKey:kTableViewEditingKey];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
self.editing = [coder decodeBoolForKey:kTableViewEditingKey];
[super decodeRestorableStateWithCoder:coder];
}
通過(guò)<UIDataSourceModelAssociation>協(xié)議使視圖對(duì)象在恢復(fù)時(shí)關(guān)聯(lián)正確的 model 對(duì)象. 當(dāng)保存狀態(tài)時(shí), 其會(huì)根據(jù) indexPath 保存一個(gè)唯一標(biāo)識(shí).
實(shí)現(xiàn)<UIDataSourceModelAssociation>協(xié)議方法:
- (NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)idx inView:(UIView *)view {
NSString *identifier = nil;
if (idx && view) {
YNCustomItem *item = [[YNItemHandler sharedStore] allItems][idx.row];
identifier = item.itemKey;
}
return identifier;
}
- (NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view {
NSIndexPath *indexPath = nil;
if (identifier && view) {
for (YNCustomItem *item in [[YNItemHandler sharedStore] allItems]) {
if ([identifier isEqualToString:item.itemKey]) {
NSInteger row = [[[YNItemHandler sharedStore] allItems] indexOfObjectIdenticalTo:item];
indexPath = [NSIndexPath indexPathForRow:row inSection:0];
break;
}
}
}
return indexPath;
}
最后記得在進(jìn)入后臺(tái)前持久化當(dāng)前的 item(實(shí)際開(kāi)發(fā)中記得用 cache(項(xiàng)目里使用 YYCache) 或者 db(項(xiàng)目里使用 FMDB) 去即時(shí)持久化視圖數(shù)據(jù), 是一個(gè)比較穩(wěn)妥的方案):
- (void)applicationDidEnterBackground:(UIApplication *)application {
BOOL success = [[YNItemHandler sharedStore] saveItems];
if (success) {
NSLog(@"成功保存所有項(xiàng)目");
} else {
NSLog(@"保存項(xiàng)目失敗");
}
}
至此, 狀態(tài)恢復(fù)基本使用已經(jīng)實(shí)現(xiàn).
測(cè)試
- 添加 n 個(gè)新的聯(lián)系人, 滑動(dòng)列表到測(cè)試位置, 讓 tableView 進(jìn)入到編輯狀態(tài). 按下
cmd + shift + h進(jìn)入 home, 用 Xcode 結(jié)束程序cmd+., 再次運(yùn)行看看是否在最后滑動(dòng)位置, 或者是否處于編輯狀態(tài). - 恢復(fù)編輯狀態(tài), 隨便進(jìn)入一個(gè)聯(lián)系人詳情, 重復(fù)上面的操作, 看看進(jìn)入程序之后是否處于上次退出前的詳情頁(yè)面.
最后, 可能說(shuō)的有些模糊, 還是結(jié)合demo看要更明白點(diǎn).