Skip to content

NavigationStack: Deep Linking (SwiftUI)

A SwiftUI guide for coding agents. Also covers programmatic navigation path, pop to root swiftui, onopenurl deep link, navigation state restoration scenestorage, handoff oncontinueuseractivity.

NavigationStack(path:) binds the stack to a mutable collection. Push by appending to the array, pop to root with removeAll, and deep-link by translating a URL into path values.

The Path Is Just an Array

SwiftUI maps the bound array to the view hierarchy and removes values when the user taps back. Your code and the back button edit the same state.

struct ShopContainerView: View {
  @State private var path: [Route] = []

  var body: some View {
    NavigationStack(path: $path) {
      List(store.products) { product in
        NavigationLink(product.title, value: Route.product(product))
      }
      .navigationDestination(for: Route.self) { route in
        switch route {
        case let .product(product):
          ProductView(product: product)
            .toolbar {
              Button("Show similar") { path.append(.related(product)) }
              Button("Back to the list") { path.removeAll() }
            }
        case let .related(product):
          ProductView(product: product.similar[0])
        case let .search(query):
          SearchView(query: query)
        }
      }
    }
  }
}

A Route enum (Hashable) lets one path array hold mixed screen types.

Never Own the Path in the App Type

State declared in the App protocol is shared by every scene, so two windows of your app would push and pop in lockstep. Hold the path in the root view or in the container view of a flow instead.

Parse the URL, build the route, append. Handoff works the same way through onContinueUserActivity.

.onOpenURL { url in
  let components = URLComponents(string: url.absoluteString)
  guard let query = components?.queryItems?.first(where: { $0.name == "query" })?.value else { return }
  path.append(.search(query))
}
.onContinueUserActivity("com.app.search") { activity in
  guard let query = activity.userInfo?["query"] as? String else { return }
  path.append(.search(query))
}

State Restoration With SceneStorage

When Route is Codable, encode the path into @SceneStorage and restore it on launch. A store type keeps the encoding and the URL handling together.

@MainActor final class NavigationStore<Route: Hashable>: ObservableObject {
  @Published var path: [Route] = []
}

extension NavigationStore where Route: Codable {
  func encoded() -> Data? { try? JSONEncoder().encode(path) }
  func restore(from data: Data) {
    path = (try? JSONDecoder().decode([Route].self, from: data)) ?? []
  }
}

struct RootView: View {
  @SceneStorage("navigation") private var path: Data?
  @StateObject private var navigation = NavigationStore<Route>(/* handlers */)

  var body: some View {
    NavigationStack(path: $navigation.path) {
      CategoriesView(categories: store.categories)
        .onOpenURL { navigation.handle($0) }
    }
    .task {
      if let path { navigation.restore(from: path) }
      for await _ in navigation.$path.values {
        path = navigation.encoded()
      }
    }
  }
}

The task restores once, then observes the published path and writes every change back to scene storage.

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-mastering-navigationstack-deep-linking" })
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