Skip to content

State Restoration (SwiftUI)

A SwiftUI guide for coding agents. Also covers scenestorage property wrapper, restore tab selection relaunch, useractivity restoration, oncontinueuseractivity, resume flow after app kill.

The system can kill a backgrounded app, and on relaunch @State and @StateObject reset to their initial values, losing where the user was. Two tools restore that context: @SceneStorage for per-scene UI state and UserActivity for resuming a marked flow.

SceneStorage

@SceneStorage persists a value in storage the system manages per scene; other scenes cannot read it, and the system restores it on relaunch. It suits tab selection, active navigation links, and sheet presentation flags.

struct RootView: View {
  @SceneStorage("selectedTab")
  private var selectedTab = 0

  @SceneStorage("bloodPressureFormShown")
  private var bloodPressureFormShown = false

  var body: some View {
    TabView(selection: $selectedTab) {
      NavigationView {
        SummaryContainerView()
          .sheet(isPresented: $bloodPressureFormShown) {
            AddBloodPressureView()
          }
      }
      .tabItem { Label("today", systemImage: "heart") }
      .tag(0)

      NavigationView { SettingsView() }
        .tabItem { Label("Settings", systemImage: "gear") }
        .tag(1)
    }
  }
}

Two cautions: the system gives no guarantee about when or how often the data persists, and it is not a place for sensitive data. @SceneStorage complements @State; it does not replace it.

UserActivity

UserActivity marks a flow with data the system preserves across launches, such as a purchase flow with a product ID. The userActivity modifier advertises the activity while a condition holds, and populates its payload.

struct PurchaseView: View {
  static let userActivity = "com.aaplab.app.purchase"
  let product: Product

  @State private var isPurchaseLinkActivated = false

  var body: some View {
    VStack {
      Text(product.title)
      NavigationLink(isActive: $isPurchaseLinkActivated) {
        CheckoutView(product: product)
      } label: {
        Label("Go to checkout", systemImage: "creditcard")
      }
    }
    .userActivity(PurchaseView.userActivity, isActive: isPurchaseLinkActivated) { activity in
      activity.title = "Purchase \(product.title)"
      activity.userInfo = ["id": product.id]
    }
  }
}

On relaunch, handle the stored activity at the app root with onContinueUserActivity and navigate back into the flow.

WindowGroup {
  RootView()
    .onContinueUserActivity(PurchaseView.userActivity) { userActivity in
      if let id = userActivity.userInfo?["id"] {
        // mutate app state and navigate to the purchase view
      }
    }
}

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-state-restoration" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems