UIViewControllers being created but not shown in state restoration

836 Views Asked by At

I have an app that is not working properly with state restoration. It previously did, but as I started moving away from the storyboard it stopped.

My app starts with a LoginViewController that is the starting view controller in my storyboard. If the login is successful, then it tries to add two FolderViewController to a navigation controller. This is so that the visible folder is one level deep already. This is done in the following code:

UINavigationController  *foldersController = [[UINavigationController alloc] initWithNavigationBarClass:nil toolbarClass:nil];
foldersController.restorationIdentifier = @"FolderNavigationController";

FolderViewController *root = [storyboard instantiateViewControllerWithIdentifier:@"FolderView"];
root.folderId = 0;
FolderViewController *fvc = [storyboard instantiateViewControllerWithIdentifier:@"FolderView"];
fvc.folderId = 1;

[foldersController setViewControllers:@[root, fvc] animated:YES];
[self presentViewController:foldersController animated:YES completion:nil];

The FolderViewController has this awakeFromNib

- (void)awakeFromNib
{
    [super awakeFromNib];
    self.restorationClass = [self class];   // If we don't have this, then viewControllerWithRestorationIdentifierPath won't be called.
}

Within the storyboard the FolderViewController has a restorationIdentifier set. When I press the Home button, the app is suspended. My restoration calls in FolderViewController are being called:

// This is being called
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
    [super encodeRestorableStateWithCoder:coder];
    [coder encodeInt64:self.folderId forKey:@"folderId"];
}

The problem now is when I try and restore. I stop the app in my debugger and then start it up again. This kicks off the restoration process.

First, my viewControllerWithRestorationIdentifierPath:coder: for my LoginViewController is called. This doesn't do much, and its use is optional. I've tried removing it and I don't have any ill effect.

+ (UIViewController*) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
    LoginViewController* vc;
    UIStoryboard* sb = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
    if (sb)
    {
        vc = (LoginViewController *)[sb instantiateViewControllerWithIdentifier:@"LoginViewController"];
        vc.restorationIdentifier = [identifierComponents lastObject];
        vc.restorationClass = [LoginViewController class];
    }
    return vc;
}

Next, the viewControllerWithRestorationIdentifierPath:coder: for my FolderViewController is called:

// This is being called
+ (UIViewController*) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
    FolderViewController* vc;
    UIStoryboard* sb = [coder decodeObjectForKey:UIStateRestorationViewControllerStoryboardKey];
    if (sb)
    {
        vc = (FolderViewController *)[sb instantiateViewControllerWithIdentifier:@"FolderView"];
        vc.restorationIdentifier = [identifierComponents lastObject];
        vc.restorationClass = [FolderViewController class];
        vc.folderId = [coder decodeInt32ForKey:@"folderId"];
    }
    return vc;
}

I've previously had a decodeRestorableStateWithCoder: as well, and it did get called. However, since it's setup in the viewControllerWithRestorationIdentifierPath:coder:, it wasn't necessary to keep it around.

All of these things are being called the appropriate number of times. But in the end, the only view controller that is displayed in the LoginViewController. Why are my FolderViewControllers not being displayed. Is there a missing setup that I need to do in my LoginViewController to attach the view controllers that I manually added previously?

Edit

After reading http://aplus.rs/2013/state-restoration-for-modal-view-controllers/ which seemed relevant, I added the following code to the App delegate:

- (UIViewController *)application:(UIApplication *)application viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents coder:(NSCoder *)coder
{
    if ([identifierComponents.lastObject isEqualToString:@"FolderNavigationController"])
    {
        UINavigationController *nc = [[UINavigationController alloc] init];
        nc.restorationIdentifier = @"FolderNavigationController";
        return nc;
    }
    else
        return nil;
}

I think the App is happier now, but it still isn't restoring properly. Now I get this error in my log:

Warning: Attempt to present <UINavigationController: 0xbaacf50> on <LoginViewController: 0xbaa1260> whose view is not in the window hierarchy!

It's something different.

2

There are 2 best solutions below

0
On

I had similar issue. My stack of view controllers was pretty simple: root view with table view -> some details view -> some edit details view. No navigation views, views are modal. And it did not work.

Turned out, the issue was that, viewControllerWithRestorationIdentifierPath() in the root view controller should look like this:

+ (UIViewController *) viewControllerWithRestorationIdentifierPath:(NSArray *)identifierComponents
                                                             coder:(NSCoder *)coder
{
        NSDictionary *myRestorableObj = [coder decodeObjectForKey:@"myRestorableObj"];

        // @todo Add more sanity checks for internal structures of @ myRestorableObj here
        if (myRestorableObj == nil)
                return nil;

        return [[UIApplication sharedApplication] delegate].window.rootViewController;
}

Instantiating new root view controller is wrong. It creates new stack of view controllers that the stored children view controllers down the stack do not belong to.

All the other children view controller should be created as usually, with the following code:

UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:@"MyChildViewController"];

Hope this helps.

1
On

You need to assign different restoration identifiers to different FolderViewController objects.

For example:

FolderViewController *folderViewController1 = // initialize object ;
FolderViewController *folderViewController2 = // initialize object ;

folderViewController1. restorationIdentifier = @"folderViewController1";
folderViewController2. restorationIdentifier = @"folderViewController2";

I tried the code above and it worked fine.