UIViewController生命周期调用与状态恢复相结合



我正试图在一个使用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页

状态恢复期间已知的事件顺序如下:

  1. application:shouldRestoreApplicationState:
  2. application:viewControllerWithRestorationIdentifierPath:coder:
  3. viewControllerWithRestorationIdentifierPath:coder:,按顺序下链
  4. viewDidLoad,按顺序下链;可能与前述内容交错
  5. decodeRestorableStateWithCoder:,按顺序下链
  6. application:didDecodeRestorableStateWithCoder:
  7. 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)

最新更新