State restoration is an essential component of scene-based applications. Apple introduced view-controller-based state restoration several years ago in iOS 6. The company deprecated view-controller-based state restoration in iOS 13 in favor of user-activity-based state restoration. In this episode, you learn what user-activity-based state restoration is and how you can adopt it in your projects.

How Does It Work?

User-activity-based state restoration isn't a difficult concept. At the heart of user-activity-based state restoration is the NSUserActivity class. It is designed to capture the state of the application or a scene of the application, and to restore the state at a later point in time. You may already use the NSUserActivity class if your application integrates with Spotlight, Handoff, or Siri.

When a scene becomes inactive, the state of the scene is preserved in an NSUserActivity instance. The user activity is assigned to the userActivity property of the scene. It is the responsibility of the system to persist the user activity.

If the system disconnects a scene or terminates the application, the user activity is used to restore the state of the scene the next time it becomes active. When that happens, the system assigns the user activity to the stateRestorationActivity property of the UISceneSession instance. The NSUserActivity instance is then used to restore the state of the scene.

The result is a seamless user experience. The user should not need to worry about the system disconnecting a scene or terminating the application in the background. That is why state restoration is an essential component of scene-based applications.

That is the theory. User-activity-based state restoration is less straightforward in practice. The state a scene should be captured by an NSUserActivity instance when the scene becomes inactive and that isn't as trivial as it seems. The scene delegate is notified when the scene becomes inactive. The scene has access to the view hierarchy, but it needs to figure out what the current state of the view hierarchy is. Which view controller is visible? What is the state of that view controller?

Leading by Example

Apple encourages developers to adopt user-activity-based state restoration and provides a sample application. Let's take a look at Apple's sample application, an application to manage a list of notes. We start with the scene(_:willConnectTo:options:) method of the SceneDelegate class. The scene delegate searches for an NSUserActivity instance by inspecting the ConnectionOptions instance and, if it doesn't have an NSUserActivity instance, it asks the session for the value of its stateRestorationActivity property.

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    // Do we have an activity to restore?
    if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
        // Setup the detail view controller with it's restoration activity.
        if !configure(window: window, with: userActivity) {
            print("Failed to restore DetailViewController from \(userActivity)")
        }
    }
}

What is happening in this method? Why is the scene delegate looking in several places for an NSUserActivity instance? The ConnectionOptions instance can contain information about the creation of the scene. If the creation of the scene was triggered by Handoff, Spotlight, or a quick action, then the ConnectionOptions instance contains a user activity that the scene delegate can use to set up the user interface of the scene, resulting in a seamless user experience.

If the scene was disconnected and is now reconnected, the state of the scene needs to be restored to guarantee an uninterrupted user experience. The user activity contains the information the scene delegate needs to restore the state of the scene. That information is stored in the stateRestorationActivity property of the scene session.

If the scene delegate obtains a reference to a user activity, then the user activity is passed to the configure(window:with:) method. This helper method accepts the UIWindow instance of the scene delegate and the user activity. Let's take a look at its implementation.

func configure(window: UIWindow?, with activity: NSUserActivity) -> Bool {
    if let detailViewController = DetailViewController.loadFromStoryboard() {
        if let navigationController = window?.rootViewController as? UINavigationController {
            navigationController.pushViewController(detailViewController, animated: false)
            detailViewController.restoreUserActivityState(activity)
            return true
        }
    }
    return false
}

The application instantiates an instance of the DetailViewController class, searches the view hierarchy for a navigation controller, pushes the detail view controller onto the navigation stack, and passes the user activity to the detail view controller. The detail view controller uses the NSUserActivity instance to configure itself.

Can you imagine what the implementation of the configure(window:with:) method would look like for a more complex project? This strategy for restoring the state of the scene works, but it isn't scalable.

Apple uses a similar technique to create an NSUserActivity instance when the scene becomes inactive. Let's take a look at the implementation of the sceneWillResignActive(_:) method. The scene delegate searches the view hierarchy for a navigation controller and checks if the topmost view controller is of type DetailViewController. If that is true, then it asks the detail view controller for its current state in the form of an NSUserActivity instance. That user activity is assigned to the scene's userActivity property. It is the responsibility of the system to persist the user activity.

func sceneWillResignActive(_ scene: UIScene) {
    if let navController = window!.rootViewController as? UINavigationController {
        if let detailViewController = navController.viewControllers.last as? DetailViewController {
            // Fetch the user activity from our detail view controller so restore for later.
            scene.userActivity = detailViewController.detailUserActivity
        }
    }
}

To understand how the DetailViewController class creates and uses the NSUserActivity class, we need to explore its implementation. We start with the detailUserActivity computed property of the DetailViewController class. The detail view controller creates an NSUserActivity instance by passing an activity type to the initializer. An activity type is nothing more than a string that is registered in the application's Info.plist. Don't worry about the activity type for now.

var detailUserActivity: NSUserActivity {
    let userActivity = NSUserActivity(activityType: DetailViewController.activityType)
    userActivity.title = "Restore Item"
    applyUserActivityEntries(userActivity)
    return userActivity
}

The detail view controller assigns a title to the user activity and, more important, it populates the userInfo property by invoking the applyUserActivityEntries(_:) method. It then returns the user activity.

The implementation of the applyUserActivityEntries(_:) method isn't too surprising. The state of the detail view controller is preserved in the userInfo dictionary of the NSUserActivity instance. The DetailViewController class uses a convenience method, addUserInfoEntries(from:), to add entries or key-value pairs to the userInfo dictionary. Bit by bit it stores its state in the userInfo dictionary of the user activity.

func applyUserActivityEntries(_ activity: NSUserActivity) {
    let itemTitle: [String: String] = [DetailViewController.activityTitleKey: detailName.text!]
    activity.addUserInfoEntries(from: itemTitle)

    let itemNotes: [String: String] = [DetailViewController.activityNotesKey: detailNotes.text!]
    activity.addUserInfoEntries(from: itemNotes)

    // We remember the item's identifier for unsaved changes.
    let itemIdentifier: [String: String] = [DetailViewController.activityIdentifierKey: detailItem!.identifier]
    activity.addUserInfoEntries(from: itemIdentifier)

    // Remember the edit mode state to restore next time (we compare the orignal note with the unsaved note).
    let originalItem = DataSource.shared().itemFromIdentifier(detailItem!.identifier)
    let nowEditing = originalItem.title != detailName.text || originalItem.notes != detailNotes.text
    let nowEditingSaveState: [String: Bool] = [DetailViewController.activityEditStateKey: nowEditing]
    activity.addUserInfoEntries(from: nowEditingSaveState)
}

The restoreUserActivityState(_:) method is invoked by the scene delegate to restore the state of the detail view controller. Notice that the DetailViewController class overrides this method. The restoreUserActivityState(_:) method is an instance method of the UIResponder class, the superclass of the UIViewController class.

override func restoreUserActivityState(_ activity: NSUserActivity) {
     super.restoreUserActivityState(activity)

    // Check if the activity is of our type.
    if activity.activityType == DetailViewController.activityType {
        // Get the user activity data.
        if let activityUserInfo = activity.userInfo {
            restoreItemInterface(activityUserInfo)
        }
    }
}

The detail view controller validates the user activity and passes its userInfo dictionary to restoreItemInterface(_:), a helper method. This method extracts the encoded state and uses it to restore the state of the detail view controller.

The solution Apple proposes works, but it isn't scalable. It falls short for larger, more complex applications. It tightly couples the various components of the project and it is difficult to test. Even though Apple's implementation works, it isn't a strategy I can recommend.

That said, I leave it up to you to decide how to implement state restoration for your project. If your application is scene-based, then you need to support user-activity-based state restoration to provide a seamless user experience.

Debugging Tips

There are a few tips you need to know about to test and debug user-activity-based state restoration. Apple mentions that the persisted state of an application is destroyed if the application is force quit by the user. This makes sense since the user explicitly terminates the application. For obvious reasons, the system also destroys any persisted user activities if the application crashes on launch.

To test and debug user-activity-based state restoration during development, Apple recommends terminating the application by pushing it to the background and stopping the debugger in Xcode. This type of termination is different than force quitting the application.

A More Robust Solution

How complex user-activity-based state restoration is for your project depends on the strategy you use to encode the state of a scene. I want to end this episode with a solution that is robust, scalable, and flexible. I have used this solution in several projects and it works every single time.

The solution I propose leverages deep links. Deep links aren't new. They have been available since the early days of iOS development. The idea is simple. Each location in the application is defined by a URL. The application should be able to take the user to a location in the application based on a URL.

If we apply this solution to the Quotes project, then we might use the following URL, quotes://quote/4. By using and registering a custom URL scheme, the deep link can be used from anywhere on the device. Even other applications can open your application. The application inspects the URL and takes the user to the appropriate location in the application.

Deep links can be used in a range of scenarios, including user-activity-based state restoration. Instead of storing random bits of data in the userInfo dictionary of the user activity, the application simply stores the URL of the current location.

We can improve this approach by delegating some of the tasks to a central object, a navigator. View controllers broadcast their life cycle and the navigator infers the URL. This approach is similar to leaving a trail of breadcrumbs and using those breadcrumbs to navigate to a location in the application.

This isn't a trivial solution, but it is scalable, testable, and easy to expand. It pays to invest time in this or a similar solution. Navigating your application also becomes trivial. You no longer need to painstakingly manage view controllers. A view controller or coordinator asks the application to open a URL and the navigator takes care of the rest.

What's Next?

State restoration isn't trivial to implement if your application has any complexity to it, but know that it is an essential component of scene-based applications. In the next episode, I show you how dependency injection and the coordinator pattern fit into the scene-based API.