Reputation: 5912
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:
When using state restoration, this is called:
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?
Upvotes: 10
Views: 4375
Reputation: 30617
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
.
Upvotes: 0
Reputation: 714
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:
application:shouldRestoreApplicationState:
application:viewControllerWithRestorationIdentifierPath:coder:
viewControllerWithRestorationIdentifierPath:coder:
, in order down the chainviewDidLoad
, in order down the chain; possibly interleaved with the foregoingdecodeRestorableStateWithCoder:
, in order down the chainapplication:didDecodeRestorableStateWithCoder:
applicationFinishedRestoringState
, in order down the chainYou 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.
Upvotes: 2
Reputation: 1707
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.
Upvotes: 0
Reputation: 21921
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.
Upvotes: 0
Reputation: 1028
Funny enough the decoding sequence is even different and exactly:
+viewControllerWithRestorationIdentifierPath:coder:
awakeFromNib
viewDidLoad
decodeRestorableStateWithCoder:
viewWillAppear
viewDidAppear
and it totally makes sense like this.
Upvotes: 5
Reputation: 924
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)
Upvotes: -1
Reputation: 113757
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.
Upvotes: 6
Reputation: 5912
@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.
Upvotes: 5