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.
Deep Links Append to the Path
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.