Skip to content

NavigationStack: NavigationPath (SwiftUI)

A SwiftUI guide for coding agents. Also covers type erased navigation stack, push mixed value types, navigationpath codable representation, persist navigation state, navigationpath vs route enum.

A typed path array ([Product]) can push only one value type. NavigationPath is the type-erased alternative: it stores any mix of Hashable values, and each navigationDestination(for:) resolves its own type. It removes the need for a Route enum.

Mixed Types in One Stack

Append products and strings to the same path; the matching destination modifier handles each.

struct ShopContainerView: View {
  @State private var path = NavigationPath()

  var body: some View {
    NavigationStack(path: $path) {
      List(store.products) { product in
        NavigationLink(product.title, value: product)
      }
      .navigationDestination(for: String.self) { query in
        SearchView(query: query)
      }
      .navigationDestination(for: Product.self) { product in
        ProductView(product: product)
          .toolbar {
            Button("Show similar") { path.append(product.query) }
          }
      }
    }
  }
}

The Codable Escape Hatch

path.codable returns a NavigationPath.CodableRepresentation you can encode to Data, and an initializer rebuilds the path from it. The gotcha: codable is nil unless every pushed value conforms to Codable, so one non-codable push silently disables persistence.

Wrap the path in an ObservableObject that owns encoding, decoding, and URL handling.

@MainActor final class NavigationStore: ObservableObject {
  @Published var path = NavigationPath()

  func handle(_ url: URL) { urlHandler.handle(url, mutating: &path) }

  func encoded() -> Data? {
    try? path.codable.map(JSONEncoder().encode)
  }

  func restore(from data: Data) {
    do {
      let codable = try JSONDecoder().decode(
        NavigationPath.CodableRepresentation.self, from: data
      )
      path = NavigationPath(codable)
    } catch {
      path = NavigationPath()
    }
  }
}

The root view restores from @SceneStorage once, then observes the path and writes every change back.

NavigationStack(path: $navigationStore.path) {
  ContentView()
    .onOpenURL { navigationStore.handle($0) }
    .task {
      if let navigationData {
        navigationStore.restore(from: navigationData)
      }
      for await _ in navigationStore.$path.values {
        navigationData = navigationStore.encoded()
      }
    }
}

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-navigationpath" })
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