我正试图在一个使用iOS 6+和情节串连板的应用程序中实现状态恢复,但我在找到防止重复调用重方法的方法时遇到了问题。
如果我只是简单地启动应用程序,那么我需要在viewDidLoad
:中设置UI
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
}
这在一个正常的、非状态的恢复世界中运行良好。现在我添加了状态恢复,在恢复一些属性后,我需要用这些属性更新UI:
- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
// restore properties and stuff
// [...]
[self setupUI];
}
现在发生的情况是,首先从viewDidLoad
调用setupUI
方法,然后从decodeRestorableStateWithCoder:
再次调用。我看不到一个可以重写的方法总是调用last。
这是方法调用的正常顺序:
- wakeFromNib
- 查看DidLoad
- 视图将出现
- 查看DidPear
当使用状态恢复时,这被称为:
- wakeFromNib
- 查看DidLoad
- decodeRestorableStateWithCoder
- 视图将出现
- 查看DidPear
我不能将对setupUI
的调用放在viewWillAppear
中,因为每次您原生返回视图时都会执行它。
如果在viewDidLoad
之前调用decodeRestorableStateWithCoder
会方便得多,因为这样您就可以使用恢复的属性。遗憾的是,事实并非如此,所以……当我知道我需要在decodeRestorableStateWithCoder
中重新完成这项工作时,我怎么能阻止在viewDidLoad
中完成这项任务呢?
如果您以程序方式进行状态恢复(即不使用情节串连板),您可以使用+ viewControllerWithRestorationIdentifierPath:coder:
,在那里初始化视图控制器,并使用编码器提供的任何信息来进行预viewDidLoad初始化。
+ (UIViewController *)viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
if ([[identifierComponents lastObject] isEqualToString:kViewControllerRestorationIdentifier]) {
if ([coder containsValueForKey:kIDToRestore]) {
// Can only restore if we have an ID, otherwise return nil.
int savedId = [coder decodeIntegerForKey:kIDToRestore];
ViewController *vc = [[ViewController alloc] init];
[vc setThingId:savedId];
return vc;
}
}
return nil;
}
我发现,试图实现状态恢复在我的代码中显示了糟糕的编程实践,比如在viewDidLoad
中打包太多。因此,虽然这很有效(如果你不使用情节串连板),但另一个选择是重构你设置视图控制器的方式。不使用标志,而是将代码段移动到它们自己的方法中,并从两个位置调用这些方法。
@property (nonatomic) BOOL firstLoad;
- (void)viewDidLoad {
[super viewDidLoad];
self.firstLoad = YES;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if (self.firstLoad) {
[self setupUI];
self.firstLoad = NO;
}
}
感谢@calvinBhai的建议。
有趣的是,解码序列甚至完全不同:
+viewControllerWithRestorationIdentifierPath:coder:
awakeFromNib
viewDidLoad
decodeRestorableStateWithCoder:
viewWillAppear
viewDidAppear
这完全有道理。
摘自《iOS 9编程:深入视图、视图控制器和框架》第386-387页
状态恢复期间已知的事件顺序如下:
application:shouldRestoreApplicationState:
application:viewControllerWithRestorationIdentifierPath:coder:
viewControllerWithRestorationIdentifierPath:coder:
,按顺序下链viewDidLoad
,按顺序下链;可能与前述内容交错decodeRestorableStateWithCoder:
,按顺序下链application:didDecodeRestorableStateWithCoder:
applicationFinishedRestoringState
,按顺序下链
你仍然不知道viewWillAppear:
和viewDidAppear:
什么时候到达,或者viewDidAppear:
是否会到达。但在applicationFinishedRestoringState
中,您可以可靠地完成视图控制器和界面的配置。
是的,如果在-viewDidLoad
之前调用-decodeRestorableStateWithCoder:
确实会更好。叹气
我将视图设置代码(取决于可恢复状态)移动到-viewWillAppear:
,并使用dispatch_once()
,而不是布尔变量:
private var setupOnce: dispatch_once_t = 0
override func viewWillAppear(animated: Bool) {
dispatch_once(&setupOnce) {
// UI setup code moved to here
}
:
}
该文档指出,"在内存不足的情况下,视图不再被清除",因此dispatch_once
在视图控制器的使用寿命内应该是正确的。
添加到berbie的答案中,
实际流量为:
initWithCoder
+viewControllerWithRestorationIdentifierPath:coder:
awakeFromNib
viewDidLoad
decodeRestorableStateWithCoder:
viewWillAppear
viewDidAppear
请注意,在initWithCoder
内部,您需要设置self.restorationClass = [self class];
。这将强制调用viewControllerWithRestorationIdentifierPath:coder:
。
我注意到在willFinishLaunchingWithOptions
中设置splitViewController.delegate
会导致更早地调用viewDidLoad
。因此,如果将其移动到两个didFinishLaunchingWithOptions
,则可以在调用viewDidLoad之前在- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder
中成功配置视图控制器。无论如何,在那里执行此操作可能会很有用,因为您将可以访问AppDelegate
对象(如persistentContainer.viewContext
),而不需要在恢复时注册该对象,这样就可以在ViewController的- (void)decodeRestorableStateWithCoder:(NSCoder *)coder
中通过引用来访问它。
对MixedCase流进行了一次更正(非常有用,谢谢),实际的调用流有点不同:
这是方法调用的正常顺序:
从Nib 唤醒
viewDidLoad
视图将出现
viewDidPear
当使用状态恢复时,这被称为:
viewControllerWithRestorationIdentifierPath(解码常规启动所需的任何数据)
从Nib 唤醒
viewDidLoad
视图将出现
viewDidPear
decodeRestorableStateWithCoder(解码可恢复的状态数据,并设置控制器UI)