UIViewController lifecycle calls in combination with state restoration
Asked Answered
R

8

10

I'm trying to implement state restoration in an app that uses iOS 6+ and storyboards, but I am having problems finding a way to prevent duplicate calls to heavy methods.

If I simply start the app, then I need to setup the UI in viewDidLoad:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupUI];
}

This works fine in a normal, non-state-restoration world. Now I've added state restoration and after restoring some properties I need to update the UI with those properties:

- (void)decodeRestorableStateWithCoder:(NSCoder *)coder {
    [super decodeRestorableStateWithCoder:coder];
    // restore properties and stuff
    // [...]
    [self setupUI];
}

So what happens now is that first the setupUI method is called from viewDidLoad, and then again from decodeRestorableStateWithCoder:. I don't see a method that I can override that's always called last.

This is the normal order of method calls:

  • awakeFromNib
  • viewDidLoad
  • viewWillAppear
  • viewDidAppear

When using state restoration, this is called:

  • awakeFromNib
  • viewDidLoad
  • decodeRestorableStateWithCoder
  • viewWillAppear
  • viewDidAppear

I can't place the call to setupUI in viewWillAppear because then it would also be executed every time you native back to a view.

It would be much handier if decodeRestorableStateWithCoder was called BEFORE viewDidLoad because then you could use restored properties. Sadly that not the case, so... how can I prevent doing the work in viewDidLoad when I know that I need to do it all over again in decodeRestorableStateWithCoder right after?

Replace answered 7/8, 2013 at 15:22 Comment(3)
I'd set a boolean to NO in viewDidLoad, and if NO, then do stuff in viewWillAppear. I am assuming you dont want to do it this way?Boring
Might actually be the most pragmatic solution, yes. It just feels "wrong" :)Replace
@NitinAlabur there is no reason to set value to NO in viewDidLoad because of default BOOL value.Padang
R
5
@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.

Replace answered 12/8, 2013 at 9:47 Comment(2)
Better to use inverse value, like self.firstLoaded, it's default value is NO and you have no reason to set it in viewDidLoad.Padang
Instead of the bool you can check if something is nil like a fetchResultsController or check the length of a text label being zero. But overall this is def the best way to go, think about it why bother setting up a view that is loaded that might never appear?Papistry
H
6

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.

Holotype answered 31/8, 2013 at 13:32 Comment(1)
This should be the accepted answer. It wasn't obvious to me initially, but you have access to the same coder in viewControllerWith... and decodeRestorableState.... Therefore, you can set up any required data for viewDidLoad in viewControllerWith....Bulldozer
R
5
@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.

Replace answered 12/8, 2013 at 9:47 Comment(2)
Better to use inverse value, like self.firstLoaded, it's default value is NO and you have no reason to set it in viewDidLoad.Padang
Instead of the bool you can check if something is nil like a fetchResultsController or check the length of a text label being zero. But overall this is def the best way to go, think about it why bother setting up a view that is loaded that might never appear?Papistry
R
5

Funny enough the decoding sequence is even different and exactly:

 +viewControllerWithRestorationIdentifierPath:coder:
 awakeFromNib
 viewDidLoad
 decodeRestorableStateWithCoder:
 viewWillAppear
 viewDidAppear

and it totally makes sense like this.

Roshan answered 30/4, 2014 at 8:49 Comment(0)
T
2

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.

Tibbetts answered 26/5, 2017 at 20:19 Comment(0)
B
0

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.

Bascio answered 4/10, 2015 at 5:22 Comment(1)
The problem with dispatch_once in this case is that the body of the dispatch_once block will only be called once during the entire invocation of the app - thus when the UIViewController is correctly deallocated, such as being moved off of a UINavigationController stack and then pushed back onto the stack, your setup code would not run the second time and the view would not be set up. You have to be VERY careful with this pattern and only include code in the dispatch_once block that doesn't need to be run if the view is deinit'd and reinstated.Kizzie
A
0

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.

Ahithophel answered 12/12, 2016 at 4:16 Comment(0)
P
0

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.

Papistry answered 10/2, 2019 at 23:47 Comment(0)
C
-1

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)

Cene answered 29/4, 2014 at 17:11 Comment(1)
This is not correct. Correct order for state restoration is: appWillLaunch -> awakeFromNib -> viewDidLoad -> decodeRestorableState -> appDidLaunch -> viewWillAppearUrina

© 2022 - 2024 — McMap. All rights reserved.