iOS APP 狀態(tài)恢復(fù)

前言

最近項(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 和本文一起看, 更好理解.

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í)有兩種情況:

  1. 新建聯(lián)系人:

    在恢復(fù)狀態(tài)時(shí)newItem:(BOOL)isNew參數(shù)傳入 YES

  2. 查看聯(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)看一下:

  1. 新建聯(lián)系人程序中的恢復(fù)標(biāo)識(shí)有:
    1. root VC 的 nav 恢復(fù)標(biāo)識(shí)
    2. 二級(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)的)
    3. 二級(jí) VC 自身的恢復(fù)標(biāo)識(shí)
  2. 查看聯(lián)系人的恢復(fù)標(biāo)識(shí)有:
    1. 根 VC 的 nav 恢復(fù)標(biāo)識(shí)
    2. 二級(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è)試

  1. 添加 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).
  2. 恢復(fù)編輯狀態(tài), 隨便進(jìn)入一個(gè)聯(lián)系人詳情, 重復(fù)上面的操作, 看看進(jìn)入程序之后是否處于上次退出前的詳情頁(yè)面.

最后, 可能說(shuō)的有些模糊, 還是結(jié)合demo看要更明白點(diǎn).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容