翻译自“View Controller Programming Guide for iOS”。
1 定义子类
使用UIViewController的自定义子类来显示应用程序内容。大部分自定义视图控制器是内容视图控制器,也就是说它们拥有自己的所有视图,并负责管理这些视图的数据。相比之下,容器视图并不拥有所有视图;有些视图由其它视图控制器管理。定义内容和容器视图控制器的大部分步骤都是一样的,这些将在下面的章节中讨论。
内容视图控制器最常见的父类如下:
- 使用UITableViewController,尤其当视图控制器的主视图是表格时。
- 使用UICollectionViewController,尤其当视图控制器的主视图是集合视图时。
- 其它所有视图控制器使用UIViewController。
容器视图控制器的父类取决于修改现有的容器类,还是创建自己的容器类。对于现有的容器,选择想要修改的视图控制器类。对于新的容器视图控制器,通常从UIViewController继承。关于创建容器视图控制器的额外信息,请参考“实现容器视图控制器”。
1.1 定义UI
在Xcode中使用故事版可视化定义视图控制器的UI。虽然也可以通过代码创建UI,但故事版是一个很好的方式来可视化视图控制器的内容,并为不同的环境(如果需要)定制视图层级结构。可视化构建UI可以快速的改变,并且不需要构建和运行应用程序就能看到结果。
图4-1展示了故事版的例子。每一个矩形区域代表一个视图控制器和它关联的视图。视图控制器之间的箭头是它们之间的关系和segue。关系连接容器视图控制器到它的子视图控制器。Segue可以在界面的视图控制器之间导航。
图4-1 包含一组视图控制器和视图的故事版
每个新项目都有一个主故事版,通常已经包括一个或多个视图控制器。通过从库中拖拽视图控制器到画布上,可以添加新的视图控制器。新视图控制器最初没有关联的类,所以必须使用Identity检查器指定一个。
使用故事版编辑器完成以下操作:
- 为视图控制器添加,排列和配置视图。
- 连接outlet和action;参考“处理用户交互”。
- 在视图控制器之间创建关系和segue;参考”Using Segues“。
- 为不同的尺寸类(size classes)定制布局和视图;参考”创建自适应界面“。
- 添加手势识别器处理视图的用户交互;参考”Event Handling Guide for iOS“。
1.2 处理用户交互
应用程序的响应者对象处理接收的事件,并采取适当的动作。尽管视图控制器是响应者对象,但它们几乎不会直接处理触摸事件。相反,视图控制器通常以下面的方式处理事件:
- 视图控制器为处理高级别事件定义动作方法。动作方法响应:
- 特定的动作。控件和一些视图调用动作方法来报告特定的交互。
- 手势识别器。手势识别器调用动作方法报告手势的当前状态。使用视图控制器处理状态变化或者响应完成的手势。
- 视图控制器监听系统或其它对象发送的通知。通知报告变化,并且是视图控制器更新状态的一种方式。
- 视图控制器作为其它对象的数据源或代理。视图控制器通常用来管理表格和集合视图的数据。也可以用做对象的代理,例如CLLocationManager对象,该对象发送更新的位置值给它的代理。
响应事件通常涉及更新视图的内容,这需要有指向这些视图的引用。视图控制器是定义之后需要修改的任何视图的outlet的好地方。使用列表4-1所示的语法声明outlet为属性。列表中的自定义类定义了两个outlets(由IBOutlet关键字指定),以及一个动作方法(由IBAction返回类型指定)。Outlet存储故事版中按钮和文件框的引用,动作方法响应按钮点击事件。
列表4-1 在视图控制器类中定义outlet和action
OBJECTIVE-C
@interface MyViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *myButton;
@property (weak, nonatomic) IBOutlet UITextField *myTextField;
- (IBAction)myButtonAction:(id)sender;
@end
SWIFT
class MyViewController: UIViewController {
@IBOutlet weak var myButton : UIButton!
@IBOutlet weak var myTextField : UITextField!
@IBAction func myButtonAction(sender: id)
}
记得在故事版中连接视图控制器的outlet和action到适当的视图。在故事版文件中连接outlet和action确保视图加载时,它们已经配置好了。关于如何在界面生成器中创建outlet和action,请参考”Interface Builder Connections Help“。关于如何在应用程序中处理事件,参考”Event Handling Guide for iOS“。
1.3 运行时显示视图
故事版让加载和显示视图控制器的视图变得简单。需要时,UIKit自动从故事版文件中加载视图。UIKit执行以下任务序列作为加载过程的一部分:
- 使用故事版文件中的信息实例化视图。
- 连接所有outlet和action。
- 指定视图控制器的view属性为根视图。
- 调用视图控制器的awakeFromNib方法。
调用该方法时,视图控制器的特征集合为空,视图可能不在最终的位置。 - 调用视图控制器的viewDidLoad方法。
使用该方法添加或移除视图,修改布局约束,加载视图的数据。
在屏幕上显示视图控制器的视图之前,UIKit在视图显示在屏幕上之前和之后,提供额外的机会来准备这些视图。具体来说,UIKit执行以下任务序列:
- 调用视图控制器的viewWillAppear:方法,告诉视图控制器视图即将在屏幕上显示。
- 更新视图的布局。
- 在屏幕上显示视图。
- 视图出现在屏幕上时,调用viewDidAppear:方法。
添加,移除,或修改视图的尺寸和位置时,记得添加和移除用于这些视图的约束。对视图层级结构做布局相关的改变,会导致布局混乱。在下一个更新周期,布局引擎使用当前布局约束计算视图的尺寸和位置,并应用这些变化到视图层级结构。
关于如何不使用故事版创建视图,请参考“UIViewController Class Reference”中的视图管理信息。
1.4 管理视图布局
当视图的尺寸和位置改变时,UIKit更新视图层级结构的布局信息。对于使用自定布局(Auto Layout)配置的视图,UIKit使用自动布局引擎根据当前约束更新布局。同时UIKit通知其它感兴趣的对象(例如当前显示的控制器),布局发生了改变,以便它们做出响应。
布局过程中,UIKit在几个点上发出通知,让你可以执行布局相关的任务。使用这些通知来修改布局约束,或者在布局约束应用后,做最后的布局调整。布局过程中,UIKit为每个受影响的视图控制器做以下事情:
- 如果需要,更新视图控制器和它的视图的特征集合。参考“什么时候发生特征和尺寸变化?”。
- 调用视图控制器的viewWillLayoutSubviews方法。
- 调用当前UIPresentationController对象的containerViewWillLayoutSubviews方法。
- 调用视图控制器根视图的layoutSubviews。该方法默认使用可用的约束来计算新的布局信息。然后遍历视图层级结构,并调用每个子视图的layoutSubviews方法。
- 应用计算好的布局信息到视图。
- 调用视图控制器的viewDidLayoutSubviews方法。
- 调用当前UIPresentationController对象的containerViewDidLayoutSubviews方法。
视图控制器可用使用viewWillLayoutSubviews和viewDidLayoutSubviews方法执行额外的更新,这些更新可能会影响布局过程。布局之前,添加或移除视图,更新视图的尺寸或位置,更新约束,或者更新其它视图相关的属性。布局之后,可以重新加载表格数据,更新其它视图的内容,或者调整视图最终的尺寸和位置。
以下是高效管理布局的技巧:
- 使用自动布局。使用自动布局创建的约束是一种灵活和简单的方式,可用在不同屏幕尺寸上放置内容。
- 利用顶部和底部的布局参考线。这些参考线确保内容总是可见的。顶部布局参考线的位置把状态栏和导航栏的高度计算在内,底部布局参考线把标签栏或工作栏的高度计算在内。
- 添加或移除视图时,记得更新约束。如果动态的添加或移除视图,记得更新相应的约束。
- 视图控制器的视图产生动画时,暂时的移除约束。当使用UIKit核心动画(Core Animation)让视图产生动画时,在动画期间移除约束,并在动画完成后添加回来。如果动画期间视图的位置或尺寸发生了变化,记得更新约束。
关于显示控制器和它们在视图控制器架构中扮演的角色,请参考“弹出和过渡过程”。
1.5 高效的管理内存
绝大部分的内存分配工作由你来决定,表格4-1列出了UIViewController的方法,最有可能在这些方法中分配或释放内存。绝大部分释放内存都涉及移除对象的强引用。通过设置指向该对象的属性和变量为nil来移除对象的强引用。
表格4-1 分配和释放内存的地方
任务 | 方法 | 讨论 |
---|---|---|
分配视图控制器需要的关键数据结构。 | 初始化方法 | 自定义的初始化方法(无论名称是否为init)总是负责把视图控制器对象放到一个已知的状态。使用这些方法来分配任何需要的数据结构,确保完成适当的操作。 |
分配或加载视图中显示的数据。 | viewDidLoad | 使用该方法加载所有要显示的数据对象。该方法调用时,视图对象已经存在,并且处于已知的状态。 |
响应低内存通知。 | didReceiveMemoryWarning | 使用该方法释放所有与视图控制器相关的非关键对象。尽可能多的释放内存。 |
释放视图控制器需要的关键数据结构。 | dealloc | 覆写的该方法只执行视图控制器类最后的清理工作。系统自动释放存储在实例变量和属性中的对象,所以不需要显式的是释放它们。 |
2 实现容器视图控制器
容器视图控制器是组合多个视图控制器内容到一个用户界面的一种方式。容器视图控制器最常用于导航和基于现有内容创建新的用户界面类型。UIKit中的容器视图控制器包括UINavigationController,UITabBarController和UISplitViewController,所有这些都方便在用户界面的不同部分导航。
2.1 设计自定义容器视图控制器
容器视图控制器几乎在所有方面都与内容视图控制器相似,它管理一个根视图和一些内容。不同的是,容器视图控制器的部分内容来源于其它视图控制器。它获得的内容仅限于其它视图控制器的视图,该视图嵌入到容器视图控制器自己的视图层级结构中。容器视图控制器设置嵌入视图的尺寸和位置,但原始的视图控制器仍然管理这些视图中的内。
设计自己的容器视图控制器时,需要理解容器和被包含的视图控制器之间的关系。视图控制器之间的关系可以帮助了解它们的内容如何在屏幕上显示,以及容器内部如何管理它们。设计过程中,询问自己以下问题:
- 容器和它的子视图控制器分别扮演什么角色?
- 同时显示多少个子视图控制器?
- 兄弟视图控制器之间的关系(如果存在的话)是什么?
- 子视图控制器如何从容器中添加或移除?
- 子视图控制器的尺寸或位置能否改变?什么条件下发生这些改变?
- 容器本身是否提供装饰或导航相关的视图?
- 容器和子视图控制器之间的通信方式是什么?除了UIViewController类定义的标准事件,容器还需要发送特定的事件到子视图控制器吗?
- 容器的外观能否以不同的方式配置?如果可以,怎么配置?
定义了各种对象之后,容器视图控制器的实现变得相对简单了。UIKit的唯一要求是在容器视图控制器和子视图控制器之间建立正式的父子关系。父子关系确保子视图控制器可以接收任何相关的系统消息。除此之外,大部分的实际工作发生在布局和管理被包含的视图中,并且不同的容器有不同的工作。可以把视图放在容器内容区域的任何位置,并设计你需要的尺寸。还可以添加自定义视图到视图层级结构中,用于提供装饰或者帮助导航。
2.1.1 例子:导航控制器
UINavigationController对象支持在层级数据集中导航。导航界面一次显示一个子视图控制器。界面顶部的导航栏显示数据的层级结构和一个返回上一级的按钮。导航到数据层级结构下一级由子视图控制器决定,可以使用表格或按钮。
视图控制器之间的导航由导航控制器和子视图控制器共同管理。当用户与按钮或子视图控制器的表格行交互时,子视图控制器请求导航控制器入栈一个新的视图控制器到视图。子视图控制器处理新视图控制器内容的配置,而导航控制器管理过渡动画。导航控制器还管理导航栏,显示一个用于关闭(dismiss)顶层视图控制器的按钮。
图5-1展示了导航控制器和它的视图的结构。顶层子视图控制器填充大部分内容区域,导航栏占据一小部分。
图5-1 导航界面的结构
在紧凑和常规环境中,导航控制器一次只显示一个子视图控制器。导航控制器调整子视图控制器尺寸来适应可用的空间。
2.1.2 例子:分割视图控制器
UISplitViewController对象在主从排列中显示两个视图控制器的内容。这种排列中,其中一个视图控制器(主控制器)决定了另外一个视图控制器显示的细节。两个视图控制器是否可见是可配置的,但也由当前环境决定。在水平方向为常规环境中,分割视图控制器并排显示两个子视图控制器,或者根据需要显示或者隐藏主视图控制器。在紧凑环境中,分割视图控制器一次只显示一个视图控制器。
图5-2展示了水平方向为常规环境时,分割视图界面和它的视图的结构。默认情况下,分割视图控制器本身只有自己的容器视图。在这个例子中,两个子视图并排显示。子视图的尺寸和主视图是否可见都是可配置的。
图5-2 分割视图界面
2.2 在界面生成器中配置容器
设计时,通过添加容器视图对象到故事版场景中来创建父子容器关系,如图5-3所示。容器视图对象是一个占位对象,表示子视图控制器的内容。使用该视图控制子视图控制器的根视图与容器中其它视图相关的尺寸和位置。
图5-3 在界面生成器中添加容器视图
加载有一个或多个容器视图的视图控制器时,界面生成器也加载这些视图相关的子视图控制器。子视图控制器必须与父视图控制器同时实例化,这样才能创建适当的父子关系。
如果不使用界面生成器设置父子容器关系,就必须通过代码添加每个子视图控制器到容器视图控制器来创建这些关系,请参考”添加子视图控制器到内容“。
2.3 实现自定义容器视图控制器
要实现一个容器视图控制器,必须在视图控制器和子视图控制器之间建立关系。必须在管理任何子视图控制器的视图之前建立这些父子关系。这样才能让UIKit知道视图控制器正在管理其孩子的尺寸和位置。可以在界面生成器或者通过代码创建这种关系。通过代码创建父子关系时,必须在设置视图控制器时,显式的添加和移除子视图控制器。
2.3.1 添加子视图控制器到内容
要把子视图控制器通过代码合并到内容中,需要完成以下几点来创建相关视图控制器之间的父子关系:
- 调用容器视图控制器的addChildViewController:方法。该方法告诉UIKit,容器视图控制器正在管理子视图控制器的视图。
- 添加子视图控制器的根视图到内容器的视图层级结构中。在这个过程中,需要设置子视图的frame的尺寸和位置。
- 添加约束来管理子视图控制器根视图的尺寸和位置。
- 调用子视图控制器的didMoveToParentViewController:方法。
列表5-1展示了容器如何嵌入子视图控制器。建立父子关系后,容器设置子视图控制器的frame,并将子视图控制器的视图添加到容器的视图层级结构中。设置子视图的frame尺寸确保视图在容器中正确的显示。添加视图后,容器调用子视图控制器的didMoveToParentViewController:方法,让子视图控制器可以响应视图的变化。
列表5-1 添加子视图控制到到容器中
- (void) displayContentController: (UIViewController*) content {
[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
在上面的例子中,只调用了子视图控制器的didMoveToParentViewController:方法。这是因为addChildViewController:方法调用了子视图控制器的willMoveToParentViewController:方法。必须自己调用didMoveToParentViewController:方法是因为该方法直到子视图控制器的视图嵌入容器的视图层级结构后才会被调用。
使用自动布局时,在添加子视图到容器的视图层级结构之后,才设置容器和子视图控制器之间的约束。约束应该只影响子视图控制器的根视图的尺寸和位置。不要改变根视图的内容或子视图层级结构中的任何视图。
2.3.2 移除子视图控制器
想从内容中移除子视图控制器,通过完成以下几点来移除视图控制器之间的父子关系:
- 使用nil值调用子视图控制器的willMoveToParentViewController:方法。
- 移除子视图控制器的根视图配置的所有约束。
- 从内容的视图层级结构中移除子视图控制器的根视图。
- 调用子视图控制器的removeFromParentViewController方法终止父子关系。
移除子视图控制器会永久的删除父子之间的关系。只有不再需要子视图控制器时才移除它。例如,当新视图控制器压入导航栈时,导航控制器不会移除当前的子视图控制器。只有在它们从栈中弹出时才会移除。
列表5-2展示了如何从容器中移除子视图控制器。使用nil值调用willMoveToParentViewController:方法,让子视图控制器有机会准备这个变化。removeFromParentViewController方法也调用了子视图控制器的didMoveToParentViewController:方法,传入的参数值为nil。设置父视图控制器为nil完成从容器中移除子视图控制器的视图。
列表5-2 从容器中移除子视图控制器
- (void) hideContentController: (UIViewController*) content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
}
2.3.3 子视图控制器之间的过渡
想要动画的用一个子视图控制器替换另一个,需要合并子视图控制器的添加和移除到过渡动画过程中。动画开始前,确保两个子视图控制器都是内容的一部分,同时让当前的子视图控制器知道它即将消失。动画过程中,移动新的子视图到相应的位置,并移除旧的子视图。动画完成后,完全移除子视图控制器。
列表5-3展示了如何使用过渡动画切换两个子视图控制器。这个例子中,新的视图控制器动画的移动到现有子视图控制器当前占据的矩形,而子视图控制器移出屏幕。动画完成后,完成块代码从容器中移出子视图控制器。这个例子中,transitionFromViewController:toViewController:duration:options:animations:completion:方法自动更新容器的视图层级结构,所以不需要自己添加和移出视图。
列表5-3 两个子视图控制器之间的过渡
- (void)cycleFromViewController: (UIViewController*) oldVC
toViewController: (UIViewController*) newVC {
// Prepare the two view controllers for the change.
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Get the start frame of the new view controller and the end frame
// for the old view controller. Both rectangles are offscreen.
newVC.view.frame = [self newViewStartFrame];
CGRect endFrame = [self oldViewEndFrame];
// Queue up the transition animation.
[self transitionFromViewController: oldVC toViewController: newVC
duration: 0.25 options:0
animations:^{
// Animate the views to their final positions.
newVC.view.frame = oldVC.view.frame;
oldVC.view.frame = endFrame;
}
completion:^(BOOL finished) {
// Remove the old view controller and send the final
// notification to the new view controller.
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
}];
}
2.3.4 管理子视图控制器的显示更新
添加子视图控制器到容器后,容器自动转发显示相关的消息到子视图控制器。通常这是你希望的行为,因为它确保所有事件都正确发送。然后,有时默认行为发送这些事件的顺序对容器没有意义。例如,如果多个子视图控制器同时改变视图的状态,可能希望合并这些变化,这样显示的回调函数能以更有逻辑的顺序同时出现。
想要接管显示回调的职责,需要在容器视图控制器中覆写shouldAutomaticallyForwardAppearanceMethods,并返回NO,如图5-4所示。
列表5-4 禁用自动转发显示
- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
return NO;
}
显示过渡发生时,适当的调用子视图控制器的beginAppearanceTransition:animated:或endAppearanceTransition方法。例如,如果容器的child属性只有一个子视图控制器引用,容器可以转发这些消息到该子视图控制器,如列表5-5所示。
列表5-5 容器显示或消失时转发显示消息
-(void) viewWillAppear:(BOOL)animated {
[self.child beginAppearanceTransition: YES animated: animated];
}
-(void) viewDidAppear:(BOOL)animated {
[self.child endAppearanceTransition];
}
-(void) viewWillDisappear:(BOOL)animated {
[self.child beginAppearanceTransition: NO animated: animated];
}
-(void) viewDidDisappear:(BOOL)animated {
[self.child endAppearanceTransition];
}
2.4 构建容器视图控制器的建议
设计,开发和测试新的容器视图控制器需要时间。尽管单个行为很简单,但控制器作为整体变得很复杂。实现自己的容器类时,考虑以下技巧:
- 只访问子视图控制器的根视图。容器应该只访问每个子视图控制器的根视图,也就是子视图控制器的view属性返回的视图。永远不要访问子视图控制器的其它视图。
- 子视图控制器应该尽可能少的了解它们的容器。子视图控制器应该关注自身的内容。如果容器允许子视图控制器影响它的行为,应该使用代理设计模式来管理这些交互。
- 优先使用常规视图设计容器。使用常规视图(而不是子视图控制器的视图)让你可以在简单的环境中测试布局约束和过渡动画。当常规视图按预期工作后,移除这些视图,并切换为子视图控制器的视图。
2.5 委托控制给子视图控制器
容器视图控制器可以委托一些自身的外观给一个或多个子视图控制器。可以通过以下几种方式委托控制:
- 让子视图控制器决定状态栏风格。在容器视图控制器中覆写其中一个或两个childViewControllerForStatusBarStyle和childViewControllerForStatusBarHidden方法,委托状态栏风格给子视图控制器。
- 让子视图控制器指定自己合适的尺寸。灵活布局的容器可以使用子视图控制器的preferredContentSize属性,来帮助决定子视图的尺寸。
3 支持辅助功能(Accessibility)
略
4 保存和恢复状态
视图控制器在状态保存和恢复过程中扮演重要的角色。应用程序挂起前,状态保存记录了应用的配置,因此随后启动时可以恢复该配置。恢复应用到上一个配置可以为用户节约时间,并提供更好的用户体验。
保存和恢复过程几乎是自动完成的,但需要告诉iOS需要保存应用程序的哪些部分。保存视图控制器的步骤如下:
- (必须)为需要保存配置的视图控制器指定恢复标识符;参考”为保存标记视图控制器“。
- (必须)启动时告诉iOS,如何创建或定位新的视图控制器对象;参考”启动时恢复视图控制器“。
- (可选)对每个视图控制器,存储用来返回视图控制器到原始配置的任何指定配置数据;参考”编码和解码视图控制器的状态“。
保存和恢复过程的概述,请参考”App Programming Guide for iOS“。
4.1 为保存标记视图控制器
UIKit只会保存你让它保存的视图控制器。每个视图控制器有一个restorationIdentifier属性,该属性默认值为nil。设置该属性为一个有效的字符串,告诉UIKit应该保存该视图控制器和它的视图。可以通过代码或故事版文件指定恢复标识符。
指定恢复标识符时,视图控制器层级结构中所有的父视图控制器也必须有恢复标识符。在保存过程中,UIKit从窗口的根视图控制器开始遍历视图控制器层级结构。如果该层级结构中的一个视图控制器没有恢复标识符,则该视图控制器,它的所有子视图控制器,以及presented视图控制器都会被忽略。
4.1.1 选择有效的恢复标识符
UIKit使用恢复标识符字符串在之后重新创建视图控制器,所以需要选择一个代码容易识别的字符串。如果UIKit不能自动创建视图控制器,它会提供该视图控制器和它所有父视图控制器的恢复标识符,让你来创建。该标识符链代表视图控制器的恢复路径,以及如何决定被请求的是哪个视图控制器。恢复路径从根视图控制器开始,包括每一个视图控制器和被请求的视图控制器。
恢复标识符通常是视图控制器的类名。如果在很多地方使用了同一个类,也许希望指定一个更有意义的值。例如,基于视图控制器管理的数据来指定一个字符串。
每一个视图控制器的恢复路径必须是唯一的。如果容器视图控制器有两个子视图控制器,容器必须为每一个子视图控制器指定唯一的恢复标识符。一些UIKit中的容器视图控制器能自动消除子视图控制器的歧义,允许子视图控制器使用相同的恢复标识符。例如,UINavigationController类根据每个子视图控制器在导航栈中的位置添加信息到子视图控制器。关于给定的视图控制器的行为的更多信息,请查看相应的类参考。
更多如何使用恢复标识符和恢复路径创建视图控制器的信息,请参考”启动时恢复视图控制器“。
4.1.2 排除视图控制器组
通过设置父视图控制器的恢复标识符为nil,从恢复过程中排除整个视图控制器组。图7-1展示了设置恢复标识符为nil对视图控制器层级结构的影响。缺少保存数据,阻止了视图控制器稍后的恢复。
图7-1 从自动保存过程中排除视图控制器
排除一个或多个视图控制器,并不会在随后的恢复中移除它们。启动时,作为应用程序默认设置部分的所有视图控制器仍然会创建,如图7-2所示。这些视图控制器按照默认配置重新创建。
图7-2 加载默认视图控制器集
从自动保存过程中排除的视图控制器,可以手动保存。通过在恢复文件(restoration archive)中保存视图控制器的引用,可以保存该视图控制器和它的状态信息。例如,如果图7-1中的应用程序代理保存了导航控制器的三个子视图控制器,它们的状态将会被保存。在恢复过程中,应用程序代理可以重新创建它们,并把它们压入导航控制器的栈中。
4.1.3 保存视图控制器的视图
有些视图有额外的跟视图相关的状态信息,而不是跟父视图控制器相关。例如,你希望保存滚动视图的滚动位置。视图控制器负责为滚动视图提供内容,滚动视图本身负责保存视觉状态。
执行以下操作来保存视图的状态:
- 为视图的restorationIdentifier属性指定一个有效的字符串。
- 使用同样具有有效恢复标识符的视图控制器的视图。
- 对于表格视图和集合视图,指定一个遵循UIDataSourceModelAssociation协议的数据源。
为视图指定恢复标识符告诉UIKIt应该把视图的状态写入保存文件。稍后恢复视图控制器时,UIKit也恢复具有恢复标识符的所有视图的状态。
4.2 启动时恢复视图控制器
启动时,UIKit视图恢复应用程序到上一个状态。此时,UIKit请求应用程序创建(或定位)视图控制器对象,该对象包括保存的用户界面。定位视图控制器时,UIKit按以下顺序搜索:
- 如果视图控制器有恢复类,UIKit请求该类提供视图控制器。UIKit调用关联的恢复类的viewControllerWithRestorationIdentifierPath:coder:方法来检索视图控制器。如果该方法返回nil,则假设应用程序不希望重新创建视图控制器,UIKit停止搜索视图控制器。
- 如果视图控制器没有恢复类,UIKit请求应用程序代理提供视图控制器。UIKit调用应用程序代理的application:viewControllerWithRestorationIdentifierPath:coder:方法搜索没有恢复类的视图控制器。如果该方法返回nil,UIKit尝试隐式查找视图控制器。
- 如果有正确恢复路径的视图控制器已经存在,UIKit使用该对象。如果应用程序在启动时创建视图控制器(通过代码或从故事版中加载),并且这些视图控制器有恢复标识符,UIKit根据它们的恢复路径隐式的找到它们。
- 如果视图控制器最初从故事版文件中加载,UIKit使用保存的故事版信息来定位并创建它。UIKit在恢复文件中保存视图控制器的故事版信息。如果恢复时,其它方式没有查找到视图控制器,UIKit使用该信息定位同一个故事版,并实例化相应的视图控制器。
为视图控制器指定一个恢复类,可以阻止UIkit隐式的搜索该视图控制器。使用恢复类让你可以进一步控制是否真的想要创建视图控制器。例如,如果恢复类决定视图控制器不应该重新创建,viewControllerWithRestorationIdentifierPath:coder:方法可以返回nil。当不存在恢复类时,UIKit尽可能找到或创建视图控制器并恢复它。
使用恢复类时,viewControllerWithRestorationIdentifierPath:coder:方法应该创建新的类实例,执行最小化的初始化,并返回对象。列表7-1展示了如何使用该方法从故事版中加载视图控制器。因为视图控制器最初从故事版中加载,所有该方法使用UIStateRestorationViewControllerStoryboardKey关键字从文件中获得故事版。该方法不会试图配置视图控制器的数据字段。该步骤发生在解码视图控制器的状态之后。
列表7-1 恢复中创建新的视图控制器
+ (UIViewController*) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
coder:(NSCoder *)coder {
MyViewController* vc;
UIStoryboard* sb = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
if (sb) {
vc = (PushViewController*)[sb instantiateViewControllerWithIdentifier:@"MyViewController"];
vc.restorationIdentifier = [identifierComponents lastObject];
vc.restorationClass = [MyViewController class];
}
return vc;
}
手动重新创建视图控制器时,重新分配恢复标识符是一个好习惯。恢复恢复标识符最简单的方式是获得identifierComponents数组的最后一项,并分配给视图控制器。
启动时从应用程序主故事版中创建的对象,不要为每个对象创建新的实例。让UIKit隐式的查找这些对象,或使用应用程序代理的application:viewControllerWithRestorationIdentifierPath:coder:方法查找已经存在的对象。
4.3 编码和解码视图控制器的状态
UIKit调用每一个要保存对象的encodeRestorableStateWithCoder:方法来保存对象的状态。恢复过程中,UIKit调用匹配的decodeRestorableStateWithCoder:方法解码状态,并应用到对象。对于视图控制器来说,这些方法的实现是可选的,但推荐实现。可以使用它们保存和恢复以下类型的信息:
- 被显示的数据的引用(不是数据本身)
- 对于容器视图控制器,指向子视图控制器的引用
- 当前选中的信息
- 对于有用户可配置的视图的视图控制器,该视图的当前配置信息。
在编码和解码方法中,可以编码任何编码器支持的对象和数据类型。除了视图和视图控制器之外的所有对象必须遵循NSCoding协议,并使用协议中方法保存状态。编码器不使用NSCoding协议保存视图和视图控制器的状态。相反,编码器保存对象的恢复标识符,并将其添加到保存对象列表,这将导致该对象的encodeRestorableStateWithCoder:方法被调用。
视图控制器的encodeRestorableStateWithCoder:和decodeRestorableStateWithCoder:方法必须在实现中调用super。调用super可以让父类保存和恢复额外的信息。列表7-2展示了这些方法的简单实现,其中保存了一个数值用于识别指定的视图控制器。
图7-2 编码和解码视图控制器的状态
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
[coder encodeInt:self.number forKey:MyViewControllerNumber];
}
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
self.number = [coder decodeIntForKey:MyViewControllerNumber];
}
编码和解码过程中,编码器对象不是共享的。每个保存状态的对象接收自己的编码器对象。使用唯一的编码器意味着不用担心关键字之间的命名空间冲突。不要自己使用UIApplicationStateRestorationBundleVersionKey,UIApplicationStateRestorationUserInterfaceIdiomKey和UIStateRestorationViewControllerStoryboardKey关键字名。UIKit使用这些关键字存储视图控制器额外的信息。
关于视图控制器编码解码方法的更多信息,请参考“UIViewController Class Reference”。
4.4 保存和恢复视图控制器的技巧
在视图控制器中添加状态保存和恢复时,考虑以下指南:
- 请记住你可能不希望保存所有视图控制器。某些情况下,保存一个视图控制器可能没有意义。例如,如果应用程序正在显示一个钱币兑换,你可能希望取消操作,并回到前一个界面。这种情况下,不应该保存视图控制器,而应该请求新的密码信息。
- 恢复过程中避免交换视图控制器类。状态保存系统编码保存的视图控制器类。恢复过程中,如果应用程序返回一个不匹配(或者不是它的一个子类)原始对象的对象,系统不会请求视图控制器解码任何状态信息。因此,交换旧视图控制器为一个完全不同的视图控制器,不会恢复对象的全部状态。
- 状态保存系统希望你按预期使用视图控制器。恢复过程依赖视图控制器之间的包含关系来重新构建界面。如果没有正确使用容器视图控制器,保存系统就会找不到视图控制器。例如,永远不要把一个视图控制器的视图嵌入一个不同的视图,除非对应的视图控制器之间存在包含关系。