State Restoration in SwiftUI

Learn how to use SceneStorage in SwiftUI to restore iOS app state. By Tom Elliott.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Restoring All The Things

In fact, it was so easy, why don't you restore state for a few more properties within the app?

Within any of the first three tabs, tap the View in Amazon button. A web view opens up showing the book in Amazon. Cold launch the app. As expected, the operating system doesn't restore the web view.

In Xcode, open BookView.swift. Find the property declaration for isShowingAmazonPage, and update it as follows:

@SceneStorage("BookView.ShowingAmazonPage") var isShowingAmazonPage = false

Notice how the identifier is different this time.

Build and run the app again. Open the Amazon page for one of the apps. Perform a cold launch, and confirm the Amazon page shows automatically after the next launch.

Restore Amazon state after relaunching

Tap Done to close the Amazon web view. Write a quick note for the book, then tap Save. The list of notes displays your note for the book. Start typing a second note. This time, before tapping Save, perform a cold launch. When the app relaunches, notice how it didn't save your draft note. How annoying!

In Xcode, still in BookView.swift, find the declaration for newNote:

@State var newNote: String = ""

And update it by adding the SceneStorage attribute to the property:

@SceneStorage("BookView.newNote") var newNote: String = ""

Another SceneStorage property, with another different identifier.

Build and run the app again. Write a draft note for a book, perform a cold start, and confirm that relaunching the app restores the draft note state.

Using state restoration to restore a draft note

Next, open CharacterView.swift. Make a similar change to update the newNote property as well, being careful to provide a different key for the property wrapper:

@SceneStorage("CharacterView.newNote") var newNote: String = ""

Build and run the app. Navigate to any character, create a draft character note and perform a cold launch. Confirm SceneStorage restores the draft note state.

State Restoration and the Navigation Stack

Tap any character to load the character detail screen. Perform a cold launch, and notice how the app didn't load the character detail screen automatically.

Hawk Notes handles navigation using a NavigationStack. This is a brand new API for iOS 16. The app stores the state of the NavigationStack in an array property called path.

Given how easy it was to restore state so far in this tutorial, you're probably thinking it's simple to add state restoration to the path property — just change the State attribute to a SceneStorage one. Unfortunately, that's not the case.

If you try it, the app will fail to compile with a fairly cryptic error message:

No exact matches in call to initializer

Attempting to save a complex model object using Scene Storage generates a compiler error

What's going on? Look at the definition for SceneStorage, and notice that it's defined as a generic struct with a placeholder type called Value:

@propertyWrapper public struct SceneStorage<Value>

Several initializers are defined for SceneStorage, all of which put restrictions on the types that Value can hold. For example, look at this initializer:

public init(wrappedValue: Value, _ key: String) where Value == Bool

This initializer can only be used if Value is a Bool.

Looking through the initializers available, you see that SceneStorage can only save a small number of simple types — Bool, Int, Double, String, URL, Data and a few others. This helps ensure only small amounts of data are stored within scene storage.

The documentation for SceneStorage gives a hint as to why this may be with the following description:

"Ensure that the data you use with SceneStorage is lightweight. Data of large size, such as model data, should not be stored in SceneStorage, as poor performance may result."

This encourages us to not store large amounts of data within a SceneStorage property. It's meant to be used only for small blobs of data like strings, numbers or Booleans.

Restoring Characters

The NavigationStack API expects full model objects to be placed in its path property, but the SceneStorage API expects simple data. These two APIs don't appear to work well together.

Fear not! It is possible to restore the navigation stack state. It just takes a little more effort and a bit of a detour.

Open BookView.swift. Add a property to hold the current scene phase underneath the property definition for the model:

@Environment(\.scenePhase) var scenePhase

SwiftUI views can use a ScenePhase environment variable when they want to perform actions when the app enters the background or foreground.

Next, create a new optional String property, attributed as scene storage:

@SceneStorage("BookView.SelectedCharacter") var encodedCharacterPath: String?

This property will store the ID for the currently shown character.

Handling Scene Changes

Finally, add a view modifier to the GeometryReader view, immediately following the onDisappear modifier toward the bottom of the file:

// 1
.onChange(of: scenePhase) { newScenePhase in
  // 2
  if newScenePhase == .inactive {
    if path.isEmpty {
      // 3
      encodedCharacterPath = nil
    }

    // 4
    if let currentCharacter = path.first {
      encodedCharacterPath = currentCharacter.id.uuidString
    }
  }

  // 5
  if newScenePhase == .active {
    // 6
    if let characterID = encodedCharacterPath,
      let characterUUID = UUID(uuidString: characterID),
      let character = model.characterBy(id: characterUUID) {
      // 7
      path = [character]
    }
  }
}

This code may look like a lot, but it's very simple. Here's what it does:

  1. Add a view modifier that performs an action when the scenePhase property changes.
  2. When the new scene phase is inactive — meaning the scene is no longer being shown:
  3. Set the encodedCharacterPath property to nil if no characters are set in the path, or
  4. Set the encodedCharacterPath to a string representation of the ID of the displayed character, if set.
  5. Then, when the new scene phase is active again:
  6. Unwrap the optional encodedCharacterPath to a string, generate a UUID from that string, and fetch the corresponding character from the model using that ID.
  7. If a character is found, add it to the path.

Build and run the app. In the first tab, tap Agatha to navigate to her character detail view. Perform a cold launch, and this time when the app relaunches, the detail screen for Agatha shows automatically. Tap back to navigate back to the book screen for The Good Hawk.

Next, tap the tab for The Broken Raven. This doesn't look right. As soon as the app loads the tab, it automatically opens the character view for Agatha, even though she shouldn't be in the list for that book. What's going on?

Broken state restoration showing Agatha in every tab