Friday, May 16, 2025
HomeiOS DevelopmentState Restoration in SwiftUI | Kodeco, the brand new raywenderlich.com

State Restoration in SwiftUI | Kodeco, the brand new raywenderlich.com

[ad_1]

Discover ways to use SceneStorage in SwiftUI to revive iOS app state.

In virtually any app conceivable, some state shall be outlined by actions the consumer has taken — the tab the consumer final chosen, the objects the consumer added to a basket or the contents of a draft message.

It will be complicated for customers in the event that they have been to cease utilizing your app for a short time, then come again to seek out the state misplaced. However that is the default conduct for iOS apps. Why? As a result of after a consumer places an app into the background, the working system could select to terminate it at any time. When this occurs, the system discards in-memory state.

There’s a characteristic in iOS the place the working system can restore state when an app is re-launched. This is named state restoration.

On this tutorial, you’ll be taught:

  • The right way to add state restoration to your SwiftUI apps.
  • The @SceneStorage property wrapper for saving state of easy knowledge.
  • Utilizing NSUserActivity to move state when launching an app.

Time to get began!

Getting Began

Obtain the challenge supplies by clicking the Obtain Supplies button on the prime or backside of this tutorial. The supplies include a challenge referred to as Hawk Notes. This tutorial builds an app for making research notes for the Shadow Skye sequence, a trilogy of epic fantasy books.

The demo app - Hawk Notes

Open the starter challenge. Now, construct and run the app utilizing Product ▸ Run from the toolbar or by clicking the Run arrow on the prime of the Venture Navigator. As soon as operating, the app shows 4 tabs: one for every of the three books and a fourth on your favourite characters.

Inside every of the primary three tabs, you will see 4 issues:

  • An outline of the ebook.
  • A hyperlink to view it on Amazon.
  • An inventory of research notes you may make in regards to the ebook, which is presently empty.
  • An inventory of the ebook’s major characters.

Faucet a personality, and the app navigates to a character element display. This display incorporates a synopsis for the character in addition to a listing of notes you’ll be able to add about that character. You may also faucet the guts to mark this character as considered one of your favorites.

Lastly, faucet the Favorites tab. There, the app lists all of the characters, break up into two sections: one on your favorites and one other for all of the others.

Swap again to Xcode and have a look across the code. Open ContentView.swift. That is the entry level into the app correct. Discover the way it defines a BookModel surroundings object. This mannequin incorporates the knowledge for every ebook and is the first knowledge supply for the app. The content material view itself shows a tab view with the 4 tabs from above — one for every ebook plus the favorites tab.

Subsequent, open BookView.swift. That is the view for displaying a ebook. The view includes a vertical stack containing an outline, a hyperlink to view the ebook on Amazon, a listing of notes and at last, a listing of characters for this ebook.

Subsequent, open CharacterView.swift. Right here, a ScrollView incorporates a VStack displaying views for the character’s avatar, a toggle swap for marking the character as a favourite, a synopsis for the character and at last, the notes for the character.

Lastly, open FavoritesView.swift. This view exhibits a listing of all the primary characters for the three books break up into two sections: first, a listing of your favourite characters, and secondly, a listing of all the opposite characters.

Swap to the Simulator and choose the third tab for The Burning Swift. Now, put the app within the background by deciding on Machine ▸ Dwelling. Subsequent, swap again to Xcode and cease the app from operating by deciding on Product ▸ Cease within the menu. Construct and run the app once more.

Observe: You will carry out the method of placing the app within the background earlier than terminating it many occasions all through the remainder of this tutorial. From this level on, if the tutorial asks you to carry out a chilly launch of the app, that is what you must do.

As soon as the app restarts, be aware how the third tab is now not chosen. That is an instance of an app that does not restore state.

Example of an app without state restoration

It is time to be taught a bit of extra about how a SwiftUI app’s Scene works now.

Understanding Scene Storage

In SwiftUI, a Scene is a container for views which have their lifecycle managed by the working system. All iOS apps begin with a single Scene. Open AppMain.swift, and you’ll see this for your self.


// 1
@major
struct AppMain: App {
  personal var booksModel = BooksModel()

  // 2
  var physique: some Scene {
    WindowGroup("Hawk Notes", id: "hawk.notes") {
      ContentView()
        .environmentObject(booksModel)
    }
  }
}

Within the code above, which is already in your app:

  1. AppMain is of kind App. The @major attribute alerts to the runtime that that is the entry level for the whole app.
  2. The physique property for an App returns a Scene that acts because the container for all views within the app.

To make state restoration very easy, Apple offers a brand new attribute you’ll be able to add to a property: @SceneStorage.

SceneStorage is a property wrapper that works very equally to the State property wrapper, which you’ll have used already. Like State, your code can each learn and write to a property attributed with @SceneStorage, and SwiftUI routinely updates any components of your app that learn from it.

SwiftUI additionally saves the worth of properties attributed with @SceneStorage into persistent storage — a database — when the app is shipped to the background. Then, it routinely retrieves and initializes the property with that worth when the app enters the foreground once more.

Due to this, SceneStorage is ideal for including state restoration to your apps.

It truly is that straightforward! So let’s now begin coding.

Saving State

It is time to add some state restoration goodness to the Hawk Notes app. Open ContentView.swift.

Close to the highest of the view, discover the road that defines the chosen tab for the app:


@State var selectedTab = ""

Replace this line to make use of the SceneStorage property wrapper like so:


@SceneStorage("ContentView.CurrentTab") var selectedTab = ""

With this alteration, you’ve got up to date the selectedTab property to make use of SceneStorage — slightly than State — with an identifier to make use of as its storage key: ContentView.CurrentTab. The identifier needs to be distinctive inside your app. This lets you create a number of SceneStorage variables which will not conflict with one another.

Construct and run the app. As soon as operating, swap to the third tab once more. Then carry out a chilly launch of the app that you just realized the right way to carry out earlier.

Restoring the selected tab

How straightforward was that! By merely altering the attribute on the selectedTab property from @State to @SceneStorage(...), your app now routinely restores the state appropriately when launched. That was straightforward!

State restoration in action

Restoring All The Issues

The truth is, it was really easy, why do not you restore state for a couple of extra properties inside the app?

Inside any of the primary three tabs, faucet the View in Amazon button. An internet view opens up displaying the ebook in Amazon. Chilly launch the app. As anticipated, the working system does not restore the net view.

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


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

Discover how the identifier is completely different this time.

Construct and run the app once more. Open the Amazon web page for one of many apps. Carry out a chilly launch, and make sure the Amazon web page exhibits routinely after the subsequent launch.

Restore Amazon state after relaunching

Faucet Achieved to shut the Amazon net view. Write a fast be aware for the ebook, then faucet Save. The listing of notes shows your be aware for the ebook. Begin typing a second be aware. This time, earlier than tapping Save, carry out a chilly launch. When the app relaunches, discover the way it did not save your draft be aware. How annoying!

In Xcode, nonetheless in BookView.swift, discover the declaration for newNote:


@State var newNote: String = ""

And replace it by including the SceneStorage attribute to the property:


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

One other SceneStorage property, with one other completely different identifier.

Construct and run the app once more. Write a draft be aware for a ebook, carry out a chilly begin, and make sure that relaunching the app restores the draft be aware state.

Using state restoration to restore a draft note

Subsequent, open CharacterView.swift. Make an analogous change to replace the newNote property as nicely, being cautious to offer a special key for the property wrapper:


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

Construct and run the app. Navigate to any character, create a draft character be aware and carry out a chilly launch. Affirm SceneStorage restores the draft be aware state.

State Restoration and the Navigation Stack

Faucet any character to load the character element display. Carry out a chilly launch, and spot how the app did not load the character element display routinely.

Hawk Notes handles navigation utilizing a NavigationStack. This can be a model new API for iOS 16. The app shops the state of the NavigationStack in an array property referred to as path.

Given how straightforward it was to revive state to this point on this tutorial, you are most likely considering it is easy so as to add state restoration to the path property — simply change the State attribute to a SceneStorage one. Sadly, that is not the case.

If you happen to attempt it, the app will fail to compile with a reasonably cryptic error message:


No actual matches in name to initializer

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

What is going on on? Have a look at the definition for SceneStorage, and spot that it is outlined as a generic struct with a placeholder kind referred to as Worth:


@propertyWrapper public struct SceneStorage<Worth>

A number of initializers are outlined for SceneStorage, all of which put restrictions on the kinds that Worth can maintain. For instance, have a look at this initializer:


public init(wrappedValue: Worth, _ key: String) the place Worth == Bool

This initializer can solely be used if Worth is a Bool.

Wanting by means of the initializers out there, you see that SceneStorage can solely save a small variety of easy sorts — Bool, Int, Double, String, URL, Knowledge and some others. This helps guarantee solely small quantities of knowledge are saved inside scene storage.

The documentation for SceneStorage provides a touch as to why this can be with the next description:

“Make sure that the information you utilize with SceneStorage is light-weight. Knowledge of enormous measurement, corresponding to mannequin knowledge, shouldn’t be saved in SceneStorage, as poor efficiency could end result.”

This encourages us to not retailer massive quantities of knowledge inside a SceneStorage property. It is meant for use just for small blobs of knowledge like strings, numbers or Booleans.

Restoring Characters

The NavigationStack API expects full mannequin objects to be positioned in its path property, however the SceneStorage API expects easy knowledge. These two APIs do not seem to work nicely collectively.

Concern not! It is doable to revive the navigation stack state. It simply takes a bit of extra effort and a little bit of a detour.

Open BookView.swift. Add a property to carry the present scene section beneath the property definition for the mannequin:


@Atmosphere(.scenePhase) var scenePhase

SwiftUI views can use a ScenePhase surroundings variable once they wish to carry out actions when the app enters the background or foreground.

Subsequent, create a brand new non-compulsory String property, attributed as scene storage:


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

This property will retailer the ID for the presently proven character.

Dealing with Scene Modifications

Lastly, add a view modifier to the GeometryReader view, instantly following the onDisappear modifier towards the underside 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 == .energetic {
    // 6
    if let characterID = encodedCharacterPath,
      let characterUUID = UUID(uuidString: characterID),
      let character = mannequin.characterBy(id: characterUUID) {
      // 7
      path = [character]
    }
  }
}

This code could seem like lots, nevertheless it’s quite simple. Here is what it does:

  1. Add a view modifier that performs an motion when the scenePhase property modifications.
  2. When the brand new scene section is inactive — which means the scene is now not being proven:
  3. Set the encodedCharacterPath property to nil if no characters are set within the path, or
  4. Set the encodedCharacterPath to a string illustration of the ID of the displayed character, if set.
  5. Then, when the brand new scene section is energetic once more:
  6. Unwrap the non-compulsory encodedCharacterPath to a string, generate a UUID from that string, and fetch the corresponding character from the mannequin utilizing that ID.
  7. If a personality is discovered, add it to the path.

Construct and run the app. Within the first tab, faucet Agatha to navigate to her character element view. Carry out a chilly launch, and this time when the app relaunches, the element display for Agatha exhibits routinely. Faucet again to navigate again to the ebook display for The Good Hawk.

Subsequent, faucet the tab for The Damaged Raven. This does not look proper. As quickly because the app masses the tab, it routinely opens the character view for Agatha, though she should not be within the listing for that ebook. What is going on on?

Broken state restoration showing Agatha in every tab

Recognizing That Books Are Distinctive

The important thing to understanding this bug is recognizing that every tab within the app makes use of the identical key for any property attributed with the SceneStorage property wrapper, and thus, all tabs share the property.

The truth is, you’ll be able to see this identical situation with all the opposite objects the app has saved for state restoration already. Attempt including a draft be aware to any of the books. Carry out a chilly launch and navigate to all three of the books. Discover how the app saves a draft for all of them.

Relying on the performance of your app, this may occasionally or might not be an issue. However for the character restoration, it most definitely is an issue. Time to repair it!

First, open ContentView.swift and replace the initialization of BookView to move within the presently chosen tab:


BookView(ebook: $ebook, currentlySelectedTab: selectedTab)

This may create a warning — however don’t be concerned — you will repair that subsequent.

Navigate again to BookView.swift, and add the next code instantly underneath the ebook property:


// 1
let isCurrentlySelectedBook: Bool

// 2
init(ebook: Binding<Ebook>, currentlySelectedTab: String) {
  // 3
  self._book = ebook
  self.isCurrentlySelectedBook = currentlySelectedTab == ebook.id.uuidString
}

On this code:

  1. You create a brand new immutable property, isCurrentlySelectedBook which can retailer if this ebook is the one presently being displayed.
  2. You add a brand new initializer that accepts a binding to a Ebook and the ID of the tab presently chosen.
  3. The physique of the initializer explicitly units the ebook property earlier than setting the isCurrentlySelectedBook property if the currentlySelectedTab matches the ID for the ebook represented by this display.

Lastly, replace the preview on the backside of the file:


BookView(
  ebook: .fixed(Ebook(
    identifier: UUID(),
    title: "The Good Hawk",
    imagePrefix: "TGH_Cover",
    tagline: "This can be a tagline",
    synopsis: "This can be a synopsis",
    notes: [],
    amazonURL: URL(string: "https://www.amazon.com/Burning-Swift-Shadow-Three-Trilogy/dp/1536207497")!,
    characters: []
  )),
  currentlySelectedTab: "1234"
)

The one distinction with the earlier preview is the addition of the currentlySelectedTab argument.

Construct the app, and now it should compile with none issues.

Updating the Scene Change

Nonetheless in BookView.swift, take away the onChange view modifier you added within the earlier part, and change it with the next:


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

      // 2
      if let currentCharacter = path.first {
        encodedCharacterPath = mannequin.encodePathFor(character: currentCharacter, from: ebook)
      }
    }
  }

  if newScenePhase == .energetic {
    if let characterPath = encodedCharacterPath,
      // 3
      let (stateRestoredBook, stateRestoredCharacter) =
        attempt? mannequin.decodePathForCharacterFromBookUsing(characterPath) {
      // 4
      if stateRestoredBook.id == ebook.id {
        // 5
        path = [stateRestoredCharacter]
      }
    }
  }
}

The construction of the above is similar to the final one you added, with some necessary variations:

  1. This time, the app solely saves the character for the ebook it shows. The app ignores this logic for all different books.
  2. Subsequent, slightly than saving the ID of the character into scene storage, you name encodePathFor(character:from:) on the ebook mannequin. You possibly can view this technique by opening BookModel.swift. It is only a easy perform that takes a Character and a Ebook and returns a String formatted as b|book_id::c|character_id. book_id and character_id are the IDs of the ebook and character, respectively.
  3. Later, when the view is relaunched, the IDs for the ebook and character are decoded after which loaded from the mannequin.
  4. If profitable, the app checks the restored ebook ID in opposition to the ebook ID for this tab. In the event that they match, it updates the path.

Construct and run the app.

This time, navigate to the primary character in every of the three books. Carry out a chilly launch from the third tab. When the app relaunches, it selects the tab for The Burning Swift and exhibits the element view for Woman Beatrice. Navigate to each the opposite ebook tabs and spot that the ebook view slightly than a personality view is proven.

Detail showing state restoration only occurs for the current tab

Understanding Energetic Customers

To date, you’ve got targeted on restoring state from a earlier session when an app launches. One other kind of state restoration can also be widespread for iOS apps — restoring from a consumer exercise.

You will use consumer exercise, represented by the NSUserActivity class, to revive state when transferring from exterior your app again into it. Examples embrace loading a selected view from a Siri search end result, deep linking from a Fast Observe or performing a Handoff to a different iOS or macOS gadget.

In every of those instances, when iOS launches your app, and a consumer exercise is offered, your app can use the knowledge from the surface app to set your state appropriately.

Including Window Dressing

Now, you will add help for a number of home windows to Hawk Notes and use NSUserActivity to load the proper content material when the app launches a brand new window.

First, it’s essential inform iOS that your app helps a number of home windows. Open the Information.plist file. Discover the row with the important thing Utility Scene Manifest, and use the disclosure indicator on the far left of the row to open the contents of the array. Replace the worth for Allow A number of Home windows to YES.

Subsequent, hover over the little up/down arrow within the heart of the final row till a plus icon seems, and click on that to create a brand new row.

Title the important thing NSUserActivityTypes, and set its kind to Array.

Use the disclosure indicator on the far left of the row to open the — presently empty — array. Then, click on the plus icon once more. This time, Xcode creates a brand new merchandise inside the NSUserActivityTypes array referred to as Merchandise 0. Set the worth of this row to:


com.raywenderlich.hawknotes.staterestore.characterDetail

This registers a brand new consumer exercise kind with iOS and tells it to open Hawk Notes when the app launches from a consumer exercise with this key.

Updating the Info.plist to support multiple windows

Subsequent, open BookView.swift.

On the very prime of the BookView declaration, instantly earlier than defining the mannequin, add the next line:


static let viewingCharacterDetailActivityType = "com.raywenderlich.hawknotes.staterestore.characterDetail"

This is identical key that you just utilized in Information.plist earlier.

Subsequent, find the initialization of the CharacterListRowView view, and add a brand new onDrag view modifier to it:


// 1
.onDrag {
  // 2
  let userActivity = NSUserActivity(activityType: BookView.viewingCharacterDetailActivityType)

  // 3
  userActivity.title = character.title
  userActivity.targetContentIdentifier = character.id.uuidString

  // 4
  attempt? userActivity.setTypedPayload(character)

  // 5
  return NSItemProvider(object: userActivity)
}

With this code, you are:

  1. Including an onDrag view modifier to every row within the listing of characters. When a row is dragged, you are then:
  2. Creating a brand new NSUserActivity with the important thing outlined earlier.
  3. Setting the title and content material of the exercise to signify the character being dragged.
  4. Setting the payload for the consumer exercise to be the Character represented by that row. setTypedPayload(_:) takes any Encodable object and, together with its decoding counterpart typedPayload(_:), permits for type-safe encoding and decoding of sorts from the UserInfo dictionary.
  5. Lastly, returning an NSItemProvider from the drag modifier. NSItemProvider is just a wrapper for passing info between home windows.

Utilizing the gadget selector in Xcode, replace your run vacation spot to an iPad Professional. Construct and run your app.

Selecting an iPad as a run destination

As soon as operating, if the iPad is in portrait mode, rotate it to panorama mode utilizing Machine ▸ Rotate Left from the menu bar.

Drag a personality to the left fringe of the iPad to set off a brand new window earlier than dropping the row.

Basic multi-window support

Your app now helps a number of home windows however, sadly, does not navigate to the chosen character.

To repair that, open BookView.swift and add a brand new view modifier to the GeometryReader:


// 1
.onContinueUserActivity(
  BookView.viewingCharacterDetailActivityType
) { userActivity in
  // 2
  if let character = attempt? userActivity.typedPayload(Character.self) {
    // 3
    path = [character]
  }
}

With this code, you:

  1. Register your BookView to obtain any consumer exercise with the important thing from earlier.
  2. Try and decode a Character occasion from the payload, utilizing the decoding half of the type-safe APIs mentioned above.
  3. Then, set the trail for use by the NavigationStack to include the Character you simply decoded.

Deep linking to the correct Character when opening a second window

Lastly, open ContentView.swift and repeat the above, however this time, restoring the state for which ebook the app ought to show within the tab view.

Add the next view modifier to the TabView:


// 1
.onContinueUserActivity(BookView.viewingCharacterDetailActivityType) { userActivity in
  // 2
  if let character = attempt? userActivity.typedPayload(Character.self), let ebook = mannequin.ebook(introducing: character) {
    // 3
    selectedTab = ebook.id.uuidString
  }
}

This code:

  1. Registers ContentView to obtain any consumer exercise tagged with the viewingCharacterDetailActivityType kind.
  2. Makes an attempt to decode a Character from the consumer exercise payload, then fetches the ebook that introduces that character.
  3. If a ebook is discovered, units the suitable tab.

Deep linking to the correct tab when opening a second window

Construct and run your app. Choose the second tab. Drag any character to create a brand new window and make sure the proper tab shows when it opens.

You probably did it! That is the tip of the tutorial and you’ve got realized all about state restoration with SwiftUI!

The place to Go From Right here?

You need to use the Obtain Supplies button on the prime or backside of this tutorial to obtain the starter and ultimate tasks.

Congratulations! You’ve got realized how straightforward it’s so as to add state restoration to your app utilizing the SceneStorage modifier and NSUserActivity.

You’ve got seen how highly effective SceneStorage will be for restoring easy knowledge sorts, but additionally how you will have a bit of extra work to do for those who plan to reuse the identical View in a number of locations, like tabs in a TabView, or if it’s essential restore complicated sorts like mannequin objects.

Alongside the best way, you touched on some extra superior subjects corresponding to generics and property declaration attributes like @State and @SceneStorage.

You’ve got additionally used the brand new NavigationStack launched with iOS 16, and seen one method to work round issues precipitated when an API with stronger kind security, NavigationStack, interacts with an API that prefers easy knowledge sorts, SceneStorage.

And most significantly, you’ve got been launched to Jamie, Agatha, Sigrid and all crucial characters from the Shadow Skye trilogy!

We hope you loved this tutorial, and in case you have any questions or feedback, please be part of the discussion board dialogue under!

[ad_2]

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments