civil-and-structural-engineering
How to Handle App State Restoration in Ios Applications
Table of Contents
Introduction
State restoration is a fundamental pillar of modern iOS development that directly influences how users perceive the reliability and polish of your application. When a user switches to another app, receives a phone call, or powers down the device, they expect to return to exactly where they left off — not to a blank screen or a lost form. Implementing proper app state restoration ensures this continuity, reducing frustration and building trust. Beyond basic user convenience, state restoration also plays a key role in maintaining a seamless experience after a system-initiated termination, such as when iOS needs to reclaim memory. Without restoration, users may lose unsaved work, navigation context, or partially completed tasks, leading to high abandonment rates. This article provides a comprehensive guide to handling app state restoration in iOS applications, covering UIKit and SwiftUI approaches, best practices, common pitfalls, and practical strategies for production-level code.
Understanding the State Restoration Process
Key Concepts and API Overview
State restoration in iOS is built on a cooperative set of UIKit APIs that allow your application to preserve and restore the state of its user interface and associated data. The core mechanism revolves around the UIStateRestoring protocol, which your view controllers and other objects adopt to encode and decode their state. Each restorable object must have a restoration identifier, a unique string that UIKit uses to match saved state with the correct object instance across launches. The state data is serialized using NSCoder, the same archiving mechanism used for NSKeyedArchiver and NSKeyedUnarchiver. UIKit automatically manages the saving and loading of the restoration archive — typically a plist file — at appropriate times, such as when the app transitions to the background or is terminated.
Scene-Based State Restoration in Modern iOS
Since iOS 13, the multitasking paradigm shifted from app-based to scene-based lifecycle with the introduction of UIScene and UISceneSession. The state restoration system was updated to support multiple scenes, each with its own restoration archive. Instead of relying solely on the app delegate’s application(_:shouldSaveApplicationState:) and application(_:shouldRestoreApplicationState:) methods, you now use the scene delegate’s corresponding methods: scene(_:stateRestorationActivityFor:) and scene(_:restoreInteractionStateWith:). Each scene gets a unique restoration archive tied to its scene session. This change requires a shift in thinking: restoration is no longer a single-app concept but a per-scene responsibility. You must ensure that restoration identifiers are unique within each scene and that the view controller hierarchy is rebuilt correctly when a scene is restored.
Implementing State Preservation
Assigning Restoration Identifiers
The first step toward state preservation is assigning restoration identifiers to every view controller you want to restore. These identifiers can be set in Interface Builder via the Restoration ID field or programmatically using the restorationIdentifier property. For example, a settings view controller might have the identifier SettingsVC. The identifier must be unique within the context of the scene or app, depending on your architecture. UIKit uses the restoration identifier to create the object hierarchy during restoration. If identifiers are missing or duplicated, restoration will fail or produce incorrect results. A good practice is to define restoration identifiers as constants in a dedicated enum or struct.
Encoding State with encodeRestorableState
Once restoration identifiers are in place, you implement encodeRestorableState(with:) in each view controller that contributes to the saved state. Inside this method, you write the data required to restore the view controller’s interface and context to the provided NSCoder instance. For example, a form view controller might save the current text field input, the selected index of a picker, or the scroll position of a table view. Only encode what is essential — avoid saving large data sets or cached images. Use encodeObject(_:forKey:), encodeBool(_:forKey:), and similar methods. Keep in mind that the coder is automatically archived and should not be used for sensitive information, as the restoration file is stored on disk.
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(selectedSegmentIndex, forKey: "selectedSegmentIndex")
coder.encode(searchQuery, forKey: "searchQuery")
}
Preserving Application State in App or Scene Delegate
In addition to per-view-controller encoding, you must tell UIKit whether to save state at the application or scene level. For apps using the app delegate lifecycle, implement application(_:shouldSaveApplicationState:) and return true. For scene-based apps, use the scene delegate’s stateRestorationActivity(for:) method to provide an NSUserActivity that carries lightweight restoration information. The heavy lifting — the full view controller state — is automatically saved by UIKit when it encodes the entire restoration hierarchy. However, you can customize what metadata is stored by implementing application(_:willEncodeRestorableStateWith:) or the scene delegate’s equivalent. This method allows you to add app-wide or scene-wide state before UIKit writes the archive.
Implementing State Restoration
Decoding State with decodeRestorableState
When the app launches and UIKit determines that a restoration should occur (based on the return value of shouldRestoreApplicationState or its scene counterpart), it rebuilds the view controller hierarchy using the storyboard or programmatic instantiation, then calls decodeRestorableState(with:) on each restorable view controller. In this method, you read back the encoded values using decodeObject(forKey:), decodeBool(forKey:), etc., and apply them to restore the user interface to its previous state. Always handle missing keys gracefully — the restoration archive may be incomplete or from a previous version of the app. Use optional binding and provide sensible defaults.
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
selectedSegmentIndex = coder.decodeInteger(forKey: "selectedSegmentIndex")
searchQuery = coder.decodeObject(forKey: "searchQuery") as? String ?? ""
}
Restoring View Controller Hierarchy
The reconstruction of the view controller hierarchy happens automatically if you use storyboards and the restoration identifier matches a storyboard identifier. For programmatically created hierarchies, you must implement application(_:viewControllerWithRestorationIdentifierPath:coder:) in the app delegate or use the scene delegate’s equivalent. This method should instantiate the view controller with the given restoration identifier path and return it. UIKit then continues traversal to restore child view controllers. Ensure that the returned view controller’s restoration identifier matches the last path component to avoid infinite recursion. For complex, multi-window apps, this step becomes crucial for restoring the correct configuration.
Handling Data Consistency
State restoration can be fragile when data dependencies change between application launches. For example, a user might have navigated to a detail view of an item that was later deleted from the server. Always validate that the restored state still makes sense before applying it. If the encoded data refers to a model object that no longer exists, revert to a default state — such as showing an empty view or a fresh list. Similarly, avoid restoring state that depends on network requests or large database fetches that may not complete in time. Instead, restore a placeholder state and asynchronously load the actual data. Use viewWillAppear or viewDidAppear to trigger these updates.
Advanced Topics
State Restoration with SwiftUI
SwiftUI provides a declarative alternative to UIKit’s state restoration. The key API is the @SceneStorage property wrapper, which automatically persists small amounts of data per scene to UserDefaults. For example, you can store the selected tab index or a search term. For more complex state, SwiftUI integrates with the Codable protocol and the NSUserActivity APIs. Use the onContinueUserActivity modifier to handle restoration from handoff or system activities. SwiftUI also supports state restoration for NavigationStack and NavigationSplitView automatically when you use appropriate identifiers. However, be aware that SwiftUI’s state restoration is more limited than UIKit’s — it does not automatically save and restore the full view controller hierarchy. You must explicitly persist important state using @SceneStorage, @AppStorage, or a custom persistence layer.
Restoration in Complex View Hierarchies
When your app contains nested split views, page view controllers, or container controllers, state restoration becomes more challenging. Each child view controller must have its own restoration identifier and encode its own state. Additionally, the parent container must properly manage the restoration of its children. For example, a UIPageViewController must implement encodeRestorableState to store the index of the current page and use the restoration identifier path to recreate the ordered children. Similarly, UITabBarController and UISplitViewController automatically handle restoration of their child view controllers if restoration identifiers are correctly assigned. Always test these hierarchies by force-quitting the app and relaunching; use the Simulator’s “Trigger Memory Warning” to simulate termination.
Asynchronous Data Loading
A common pitfall is trying to restore state that depends on asynchronous data, such as a table view that was showing results from a network request. The restoration process occurs synchronously during launch, before network calls are initiated. If you attempt to populate a view controller with saved data that is not yet available, the restoration will appear incomplete or blank. The solution is to restore only the metadata needed to refetch the data (e.g., the identifier of the last viewed item, or a query string). Then, in viewDidLoad or viewWillAppear, initiate an asynchronous fetch and update the UI once data arrives. You can also combine state restoration with background tasks to pre-warm caches, but keep the user interface responsive.
Testing State Restoration
Thorough testing of state restoration is essential but often overlooked. The easiest way to test is to use the iOS Simulator: launch your app, navigate to a specific state, press the home button, then stop the app from Xcode. Relaunch the app and verify that the state is restored. For more realistic scenarios, enable the “Simulate Memory Warning” option in the Simulator’s Debug menu while the app is in the background. This forces the system to terminate the app, and upon relaunch, state restoration should kick in. Additionally, test with different device orientations, multitasking split views, and after interruptions like phone calls. Use the console logs to check for restoration-related warnings or errors, such as missing restoration identifiers.
Best Practices for Robust State Restoration
- Keep restoration data minimal: Encode only the information required to reconstruct the user’s context — avoid saving large blobs, images, or entire model graphs.
- Use unique and stable restoration identifiers: Hardcode strings or define constants; never use auto-generated IDs that may change between builds.
- Validate restored state: Always check that restored data is still valid and that referenced model objects exist before applying the state.
- Handle versioning gracefully: If your app’s data model changes, implement version checks in your encoding/decoding logic to avoid crashes.
- Combine with NSUserActivity: For lightweight restoration (e.g., continuing a FaceTime call or a search query), use
NSUserActivityalongside full state restoration for best results. - Respect user privacy: Never encode sensitive data such as passwords, credit card numbers, or personal health information in the restoration archive.
- Set restoration class for view controllers instantiated programmatically: If you create view controllers without a storyboard, set their
restorationClassproperty to a class that knows how to instantiate them. - Test with launch arguments: Use the
-UIStateRestorationUIStateRestorationUnarchiverOutputPathlaunch argument to inspect the restoration archive during development.
Common Pitfalls and How to Avoid Them
- Missing restoration identifiers on child view controllers: Every view controller that appears in the restored hierarchy must have a restoration identifier, including those embedded in navigation controllers or tab bar controllers. Otherwise, UIKit skips the entire subtree.
- State encoded but never decoded because the view hierarchy changed: If you restructure your storyboard or change the order of view controllers, previously encoded state may become orphaned. Mitigate this by using optional decoding and falling back to defaults.
- Restoration attempted on a fresh install or after app update: State restoration is only invoked when the app was previously running. After a fresh install or an update that clears the sandbox, no archive exists. Your code should handle this gracefully without crashes.
- Overwriting state during decoding: Avoid calling
encodeRestorableStatefrom withindecodeRestorableState. Keep encoding and decoding completely separate. - Ignoring scene connections and disconnections: In scene-based apps, state restoration applies per scene. Handle
sceneDidDisconnectto clean up scene-specific state or refresh the restoration archive. - Restoration of asynchronous operations too early: Do not rely on network calls completing before restoration finishes. Use placeholders and load data asynchronously after the UI is displayed.
External Resources and Further Reading
For an in-depth understanding of UIKit’s state restoration, start with Apple’s official documentation on Preserving Your App’s UI. The WWDC 2014 session “State Restoration in Practice” covers many real-world scenarios. For SwiftUI-specific guidance, refer to the SceneStorage documentation. A comprehensive third-party tutorial can be found on Ray Wenderlich’s site. Finally, the Apple View Controller Programming Guide (archived but still relevant) contains detailed advice on restoration identifiers and the unarchiving process.
Conclusion
App state restoration is not an optional feature — it is an expected part of a well-crafted iOS application. Users invest time in navigating your interface, filling forms, and exploring content; the ability to seamlessly resume that experience after an interruption directly impacts user satisfaction and retention. By understanding the UIKit state restoration APIs, implementing proper restoration identifiers, encoding only necessary data, and handling edge cases like data changes and asynchronous operations, you can deliver a robust experience that feels reliable and polished. Whether you are maintaining a legacy UIKit app or building a new SwiftUI interface, the principles remain the same: anticipate the user’s context, preserve it thoughtfully, and restore it gracefully. With careful planning and thorough testing, state restoration becomes a natural part of your development workflow rather than an afterthought.
Remember that the goal is not to replicate every pixel of the previous session but to recreate the user’s intent. A successful state restoration leaves the user wondering whether the app ever really closed — and that is the highest compliment.