State Restoration not restoring root view controller

I'm trying to implement UI state restoration in an old Objective-C UIKit app that does not use scenes and also does not have a storyboard. I have implemented the correct AppDelegate methods -- application:shouldSaveSecureApplicationState: and application:shouldRestoreSecureApplicationState: -- and I can see that they are being called when expected.

I have also implemented the state restoration and UIViewControllerRestoration viewControllerWithRestorationIdentifierPath:coder: methods for the view controllers I want to persist and restore; along with correctly setting their restorationIdentifier and restorationClass properties. I can also see that those are being called when expected.

I've also installed the restorationArchiveTool and have verified that the persisted archive contains the expected view controllers and state.

However, once state restoration is complete, the window's rootViewController property is still nil and has not been assigned to the view controller that has the restorationIdentifier of the rootViewController at the time the state is persisted.

I found this sample app that does what I want to do -- persist and restore the state of the view controllers without the use of storyboards. Running that I verified that it does in fact work as expected. https://github.com/darrarski/iOS-State-Restoration

The difference between what that app does and what mine does, is that it always creates the top level view controllers in application:willFinishLaunchingWithOptions:.

    func application(_ application: UIApplication,
                     willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {

        let viewControllerA = DemoViewController()
        viewControllerA.title = "A"

        let navigationControllerA = UINavigationController(rootViewController: viewControllerA)
        navigationControllerA.restorationIdentifier = "Navigation"

        let viewControllerB = DemoViewController()
        viewControllerB.title = "B"

        let navigationControllerB = UINavigationController(rootViewController: viewControllerB)
        navigationControllerB.restorationIdentifier = "Navigation"

        let tabBarController = UITabBarController()
        tabBarController.restorationIdentifier = "MainTabBar"
        tabBarController.viewControllers = [navigationControllerA, navigationControllerB]

        window = UIWindow(frame: UIScreen.main.bounds)
        window?.restorationIdentifier = "MainWindow"
        window?.rootViewController = tabBarController

        return true
}

Unfortunately, my app is more dynamic and the rootViewController is determined after launch. When I updated the app to be structured more like mine, I see that it fails the same as mine and no longer restores the rootViewController.

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    var didRestoreState = false

    func application(_ application: UIApplication,
                     willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.restorationIdentifier = "MainWindow"
        return true
    }

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        if !didRestoreState {
            let viewControllerA = DemoViewController()
            viewControllerA.title = "A"

            let navigationControllerA = UINavigationController(rootViewController: viewControllerA)
            navigationControllerA.restorationIdentifier = "Navigation"

            let viewControllerB = DemoViewController()
            viewControllerB.title = "B"

            let navigationControllerB = UINavigationController(rootViewController: viewControllerB)
            navigationControllerB.restorationIdentifier = "Navigation"

            let tabBarController = UITabBarController()
            tabBarController.restorationIdentifier = "MainTabBar"
            tabBarController.viewControllers = [navigationControllerA, navigationControllerB]

            window?.rootViewController = tabBarController
        }
        window?.makeKeyAndVisible()
        return true
    }

    func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool {
        let libraryDirectory = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first
        let appStateUrl = libraryDirectory?.appendingPathComponent("Saved Application State")
        NSLog("Restoration files: \(appStateUrl?.path ?? "none")")
        return true
    }

    func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool {
        return true
    }

    func application(_ application: UIApplication, didDecodeRestorableStateWith coder: NSCoder) {
        didRestoreState = true
    }
}

I don't really understand why this doesn't work since the archive file looks identical in both cases, and the methods used to create and restore state of the view controllers are being called in both cases.

Is there something else I need to do to correctly restore the view controllers without having to create them all before state restoration begins?

@taylesworth

Have you reviewed Restoring your app’s state sample project and article? It walks you through how preserve your appʼs state and restore the app to that previous state on subsequent launches.

You wrote:

I'm trying to implement UI state restoration in an old Objective-C UIKit app that does not use scenes and also does not have a storyboard.

You should first adopt scene-based life-cycle, before proceeding any further. In iOS 13 and later, the recommend approach is to use UISceneDelegate objects to respond to life-cycle events.

Specifying the scenes your app supports would walk you through on how to enable scene support.

Yes, I have gone through all of Apple's documentation including the sample project you mentioned. That project and its related article say, "If your app doesn’t support scenes, use the view-controller-based state restoration process to preserve the state of your interface instead."

Unfortunately moving to UISceneDelegate would be difficult for this project so using the view-controller-based state restoration would be ideal. However Apple's sample project uses a storyboard for this which, as I said, our app does not.

The GitHub project I linked to above does not use the UISceneDelegate APIs and works without a storyboard. Unfortunately it seems to require that the view controllers already exist even in the case where they are being restored. Which seems ... odd, given that viewControllerWithRestorationPath:coder is being called correctly.

What I need to understand is how to actually assign the decoded rootViewController to the window's rootViewController property. As I said, I can see that all of the view controllers that were encoded to the archive are correctly created by viewControllerWithRestorationIdentifierPath:coder: during the restoration process at startup.

State Restoration not restoring root view controller
 
 
Q