UIViewController lifecycle calls in combination with state restoration

别等时光非礼了梦想. 提交于 2019-12-03 06:20:41

If you're doing state restoration programatically (i.e. not using storyboards), you can use + viewControllerWithRestorationIdentifierPath:coder:, init the view controller there and use whatever you need from the coder to do your pre-viewDidLoad initialization.

+ (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;
}

I've found that trying to implement state restoration has shown up bad programming practices in my code, like packing too much into viewDidLoad. So while this works (if you're not using storyboards), the other option is to refactor how you're setting up your view controllers. Instead of using a flag, move code pieces to their own methods and call those methods from both places.

Funny enough the decoding sequence is even different and exactly:

 +viewControllerWithRestorationIdentifierPath:coder:
 awakeFromNib
 viewDidLoad
 decodeRestorableStateWithCoder:
 viewWillAppear
 viewDidAppear

and it totally makes sense like this.

@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;
    }
}

Thanks to @calvinBhai for the suggestion.

From the book "Programming iOS 9: Dive Deep into Views, View Controllers, and Frameworks" pages 386-387

The known order of events during state restoration is like this:

  1. application:shouldRestoreApplicationState:
  2. application:viewControllerWithRestorationIdentifierPath:coder:
  3. viewControllerWithRestorationIdentifierPath:coder:, in order down the chain
  4. viewDidLoad, in order down the chain; possibly interleaved with the foregoing
  5. decodeRestorableStateWithCoder:, in order down the chain
  6. application:didDecodeRestorableStateWithCoder:
  7. applicationFinishedRestoringState, in order down the chain

You still don’t know when viewWillAppear: and viewDidAppear: will arrive, or whether viewDidAppear: will arrive at all. But in applicationFinishedRestoringState you can reliably finish configuring your view controller and your interface.

Yes, it would indeed be nicer if -decodeRestorableStateWithCoder: were called before -viewDidLoad. Sigh.

I moved my view setup code (which depends on restorable state) to -viewWillAppear: and used dispatch_once(), instead of a boolean variable:

private var setupOnce: dispatch_once_t = 0

override func viewWillAppear(animated: Bool) {
    dispatch_once(&setupOnce) {
        // UI setup code moved to here
    }

    :
}

The documentation states that "views are no longer purged under low-memory conditions" so dispatch_once should be correct for the lifetime of the view controller.

Adding to berbie's answer,

The actual flow is:

 initWithCoder
 +viewControllerWithRestorationIdentifierPath:coder:
 awakeFromNib
 viewDidLoad
 decodeRestorableStateWithCoder:
 viewWillAppear
 viewDidAppear

Be aware that inside initWithCoder, you need to set self.restorationClass = [self class]; This will then force viewControllerWithRestorationIdentifierPath:coder: to be called.

I noticed that setting the splitViewController.delegate in willFinishLaunchingWithOptions causes viewDidLoad to be called even earlier. So if you move that to both didFinishLaunchingWithOptions then you can successfully configure your view controller inside - (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray<NSString *> *)identifierComponents coder:(NSCoder *)coder before viewDidLoad is called. It might be useful for you to do it there anyway since you'll have access to AppDelegate objects like persistentContainer.viewContext rather than need to register that object with restoration so it could have been accessed by reference in the ViewController's - (void)decodeRestorableStateWithCoder:(NSCoder *)coder.

One correction to MixedCase flow (which was very helpful, thank), the actual call flow is a bit different :

This is the normal order of method calls:

awakeFromNib

viewDidLoad

viewWillAppear

viewDidAppear

When using state restoration, this is called:

viewControllerWithRestorationIdentifierPath (decode any data that is needed for regular start-up)

awakeFromNib

viewDidLoad

viewWillAppear

viewDidAppear

decodeRestorableStateWithCoder (decode restorable state data, and set your controller UI)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!