如何实现 UITabbarController 的 State Preservation?
最近在看ios programming - the big nerd ranch guide 这本书,其中第24章介绍了如何使用系统接口来实现 State Restoration. 示例部分介绍的是如何针对 UINavigationController 来进行保存和还原状态, 然后额外的练习题部分是 UITabbarController 的状态保存和恢复,可是在这里却一直遇到问题, 导致程序返回时UITabbarController始终无法还原状态,本文记录下如何使用State Restoration和UITabbarController所需的额外处理。
首先, 我们需要告诉系统我们想要实现状态的保存和恢复, 在 AppDelegate 中实现如下两个方法:
- (BOOL)application:(UIApplication *)application shouldSaveApplicationState:(NSCoder *)coder {
return YES;
}
- (BOOL)application:(UIApplication *)application shouldRestoreApplicationState:(NSCoder *)coder {
return YES;
}
其次, 我们需要给每个页面赋予一个独有的restorationIdentifier, 两个子页面还需要设置restorationClass, 用于还原状态时重新生成view controller. 代码如下:
HypnosisViewController *hvc = [HypnosisViewController new];
ReminderViewController *rvc = [ReminderViewController new];
UITabBarController *tabVC = [[UITabBarController alloc] init];
tabVC.viewControllers = @[hvc, rvc];
tabVC.restorationIdentifier = NSStringFromClass([tabVC class]);
...
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.tabBarItem.title = @"Hypno";
self.tabBarItem.image = [UIImage imageNamed:@"Hypno"];
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [self class];
}
return self;
}
...
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
NSLog(@"%s line:%d",__FUNCTION__, __LINE__);
self.tabBarItem.title = @"Reminder";
self.tabBarItem.image = [UIImage imageNamed:@"Time"];
self.restorationIdentifier = NSStringFromClass([self class]);
self.restorationClass = [self class];
}
return self;
}
接着,我们需要在子页面中实现UIViewControllerRestoration协议,同时还要重写UIViewController自带的用于 Restoration 的两个方法, 下面以HypnosisViewController为例,这里在收到系统需要保存状态消息时,写入当前textField的值,并且在还原状态时读取,确保用户下次进入程序时仍能还原回之前的场景:
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
UIViewController *vc = [self new];
return vc;
}
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[coder encodeObject:self.textField.text forKey:@"textField"];
[super encodeRestorableStateWithCoder:coder];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
self.textField.text = [coder decodeObjectForKey:@"textField"];
[super decodeRestorableStateWithCoder:coder];
}
最后, 我们只提供了两个子页面如何进行还原, 还需要处理下UITabbarController, 如果我们不提供 Controller 的restorationClass属性, 我们可以在application:viewControllerWithRestorationIdentifierPath:coder:中再次处理部分页面的恢复事件,代码如下:
UIViewController *vc = [UITabBarController new];
vc.restorationIdentifier = [identifierComponents lastObject];
//因为仅当该 Controller 为 UITabbarController 时, identifierComponents数组才会只有一个值
if (identifierComponents.count == 1) {
self.window.rootViewController = vc;
}
return vc;
至此,我们理论上算是已经完成了所有的保存和恢复事件,起码书里面针对UINavigationController的示例代码部分仅包含这些步骤就已经实现了这个功能,那么,UITabbarController是不是一样呢?
问题
运行程序,为了检验Restoration 的效果,我们进入第二个页面,并且设置一个日期,之后进入后台或者停止调试触发保存事件,当我们再次启动程序时, 可以看到这个页面一闪而过:

之后就变为了这样:

事实证明,我们的 UITabbarController 并没有很好地还原为之前的状态,甚至页面变成全空的了,为什么会变成这样呢?
探究
首先,通过启动程序时的 loading 图可以确定,我们的程序确实已经保存下了之前退出时候的页面状态,后面的白屏证实还原状态这里出了问题,那么即使UITabbarController无法很好地还原,为什么再次启动后会连子页面都不见了呢?
View Debug
这里, 我们首先想到使用 ios8以后引入的View Debugging来查看 View 层级, 如下图:

这里我们可以看到, View 层级中,并没有任何子页面,仅仅包含一个 UITabbar 而已,也就是说,我们的两个子页面根本没有添加到 UITabbarController 中, 这又是怎么一回事呢?
查阅文档
我们看下苹果官方文档对这里是如何描述的:
In iOS 6 and later, if you assign a value to this view controller’s restorationIdentifier property, it preserves a reference to the view controller in the selected tab. At restore time, it uses the reference to select the tab with the same view controller.
看起来好像没有任何问题,只是简单地说了指定 UITabbarController 的restorationIdentifier后,它会记录下当前选中的子页面,然后在下次启动的时候恢复过来. 接着看下去,App Programming Guide for iOS是这么说的:
Although UIKit helps restore the individual view controllers, it does not automatically restore the relationships between those view controllers. Instead, each view controller is responsible for encoding enough state information to return itself to its previous state. For example, a navigation controller encodes information about the order of the view controllers on its navigation stack. It then uses this information later to return those view controllers to their previous positions on the stack. Other view controllers that have embedded child view controllers are similarly responsible for encoding any information they need to restore their children later.
Note: Not all view controllers need to encode their child view controllers. For example, tab bar controllers do not encode information about their child view controllers. Instead, it is assumed that your app follows the usual pattern of creating the appropriate child view controllers prior to creating the tab bar controller itself.
这两段话大致是说, UIKit 虽然帮我们处理了恢复单独页面的事情,但是并不会帮我们恢复页面之间的逻辑关系, 那些包含多个子页面的 Controller 需要自己记录子页面间的逻辑关系并且在恢复时处理它们. 这里还专门说了 UINavigationController 会记录下页面的层级关系并且在再次创建时恢复它,而第二段话说 UITabbarController 却不会记录子页面的顺序, 需要我们自己进行处理. 同时,我们可以在恢复页面时候自行变更 UITabbarController 子页面的顺序.
解决问题
有了以上知识点,我们就知道了如何还原一个 UITabbarController 页面了,除了赋值restorationIdentifier以外,我们还需要在还原时候自行添加子页面.
AppDelegate.m
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
// 1.
HypnosisViewController *hvc = [HypnosisViewController new];
ReminderViewController *rvc = [ReminderViewController new];
UITabBarController *tabVC = [[UITabBarController alloc] init];
tabVC.viewControllers = @[hvc, rvc];
tabVC.restorationIdentifier = NSStringFromClass([tabVC class]);
self.window.rootViewController = tabVC;
return YES;
}
这样,每次程序启动时,即创建一个新的 UITabbarController, 同时赋予它两个子页面,
新增了两处代码,用于指定新建的 UITabbarController 的子页面, 之后对两个子页面的+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder方法改动如下:
HypnosisViewController.m
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
// First object in the array stores the root view controller, which is the tabBarController
UITabBarController *rootViewController = (UITabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;
return rootViewController.viewControllers[0];
}
ReminderViewController.m
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
// First object in the array stores the root view controller, which is the tabBarController
UITabBarController *rootViewController = (UITabBarController *)[UIApplication sharedApplication].delegate.window.rootViewController;
return rootViewController.viewControllers[1];
}
用于告诉系统,这两个子页面不需要再次创建,直接返回当前 UITabbarController 子页面即可.
再次运行程序,发现已经能够正常返回到第二个页面了,大功告成!
扩展
之前文档中有写到, UITabbarController 仅保存了当前选中的页面的指针,而没有保存页面的顺序, 那么,也就是说,我们可以在还原时候重新自定义tab 的顺序, 对代码进行以下改动:
- AppDelegate.md
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
...
tabVC.viewControllers = @[rvc, hvc];
...
return YES;
}
- HypnosisViewController.m
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
...
return rootViewController.viewControllers[1];
}
- ReminderViewController.m
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder {
...
return rootViewController.viewControllers[0];
}
再次运行, 得到:

我们成功的在还原页面状态时候变更了页面顺序,而且UITabbarController 仍然选中为之前保存的状态, 大功告成!
参考文档
- UITabBarController Class Reference
- Help needed for silver challenge
- App Programming Guide for iOS
- http://aplus.rs/2013/state-restoration-in-ios-6-without-storyboards/
如何实现 UITabbarController 的 State Preservation?的更多相关文章
- iOS面试题大全-点亮你iOS技能树
所有的内容大部分来自于网络的搜集,所以我不是一个创造者,而是一个搬运工.我尽量把题目,尤其是参考答案的出处列明.若有任何疑问,建议,意见,请联系我. 第一部分面试题来源于iOS-Developer-I ...
- IOS框架和服务
在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,以及图像.声音文件等其他资源.共享资源库定义应用程序可以调用的函数和方法. iOS为应用程序开发提供了许多可使用的框架 ...
- [iOS 主要框架的总结]
原文地址:http://blog.csdn.net/GooHong/article/details/28911301 在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件, ...
- iOS 的基本框架
在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,以及图像.声音文件等其他资源.共享资源库定义应用程序可以调用的函数和方法. iOS为应用程序开发提供了许多可使用 ...
- ios 总结
1 ocoa Touch Layer{ App Extensions https://developer.apple.com/library/ios/documentation/General/Con ...
- ios 框架学习笔记
ios主要的系统层次: 一.Cocoa Touch 层:创建应用程序主要使用的框架. 1.关键技术: AirDrop:实现应用间通信. Text Kit:处理文本和排版. UIKit Dynamics ...
- 1230.2——iOS准备(阅读开发者文档时的笔记)
1.程序启动的过程 .在桌面找到相应的应用的图标 点击图标 .main函数 UIApplication类Every app has exactly one instance of UIAp ...
- iOS苹果官方Demo合集
Mirror of Apple’s iOS samples This repository mirrors Apple’s iOS samples. Name Topic Framework Desc ...
- iOS操作系统的层次结构
iOS操作系统4层结构,如下表 可触摸层 Cocoa Touch layer 媒体层 Media layer 核心服务层 Core Services layer 核心操作系统层 Core OS lay ...
随机推荐
- table 增加或删除一行
转载请注明来源:https://www.cnblogs.com/hookjc/ <HTML><SCRIPT LANGUAGE="JScript">funct ...
- java基础-抽象类与接口(转)
抽象类与接口是java语言中对抽象概念进行定义的两种机制,正是由于他们的存在才赋予java强大的面向对象的能力.他们两者之间对抽象概念的支持有很大的相似,甚至可以互换,但是也有区别. 一.抽象类 ...
- 在zabbix中实现发送带有图片的邮件和微信告警
1 python实现在4.2版本zabbix发送带有图片的报警邮件 我们通常收到的报警,都是文字,是把动作中的消息内容当成了正文参数传给脚本,然后邮件或者微信进行接收,往往只能看到当前值,无法直观的获 ...
- JVM收藏的文章
JAVA 内存泄露详解(原因.例子及解决) https://blog.csdn.net/anxpp/article/details/51325838 JVM内存区域划分Eden Space.Survi ...
- docker基础——3.存储卷
把宿主机的目录或文件链接到容器的目录或文件,可以避免写时复制对高I/O操作的影响,也避免容器销毁时数据丢失. 1. 只指定容器的目录位置,-v docker run -it --name bbox1 ...
- CSS解决父级边框坍塌的问题
1. 浮动元素后面增加空的div 首先在父级标签内添加如下<div>标签 <div id="clear"></div> 然后在CSS中对该标签进 ...
- Ubuntu - root, sudo, su, passwd
1.rootubuntu中默认是不使用root账户的,当然也是可以开启并设置为默认登录账户的,但ubuntu不建议使用而已,毕竟root账户拥有所有权限,可能会出现一些误操作之类.在普通账户中,如果遇 ...
- PHP的加密方法汇总
PHP的加密主要有4种方法,除此之外还有一种是URL的加密和解密.希望可以对你们开发有用. 顺带,我会在后面把我整理的一整套CSS3,PHP,MYSQL的开发的笔记打包放到百度云,有需要可以直接去百度 ...
- Solution -「JOISC 2020」「UOJ #509」迷路的猫
\(\mathcal{Decription}\) Link. 这是一道通信题. 给定一个 \(n\) 个点 \(m\) 条边的连通无向图与两个限制 \(A,B\). 程序 Anthon ...
- Linux上大文件切割以及批量并发处理
一.环境说明 某次项目需求中,在Linux上有批文本文件,文件文件都有几个G大,几千万行的数据.无论在Linux和Windows打开这么大的文件,基本上打开要卡半天,更别说编辑. 因此想到使用spli ...