SwiftUI NavigationStack: Type-Safe, Programmatic Navigation
Drive push navigation with values instead of booleans — navigationDestination, a bindable NavigationPath, deep-link restoration, and a clean migration off the deprecated NavigationView.
NavigationStack is SwiftUI's iOS 16+ replacement for NavigationView. You push screens by presenting data: a NavigationLink(value:) appends a value to the stack, and navigationDestination(for:) maps each value type to a destination view. Bind a NavigationPath with NavigationStack(path:) for full programmatic control — append to push, removeLast to pop, and reassign the whole path to restore a deep link.
Value-based destinations
The core idea is separation: a NavigationLink(value:) only says which value should be pushed, while navigationDestination(for:) declares how to render each type of value. Any Hashable value works, so you can register multiple destination types on the same stack — one for Item, another for a settings route enum. Because links carry data rather than views, you can generate them in a ForEach without eagerly constructing every destination, which keeps large lists efficient.
NavigationStack {
List {
ForEach(products) { product in
NavigationLink(value: product) { Text(product.title) }
}
NavigationLink("Settings", value: Route.settings)
}
.navigationDestination(for: Product.self) { ProductDetail(product: $0) }
.navigationDestination(for: Route.self) { route in
switch route {
case .settings: SettingsView()
}
}
}Deep-link restoration
Because the entire back stack is expressed as a path of values, restoring a deep link is just building the right array. Parse an incoming URL into your route values and assign them to the path in order; SwiftUI pushes the whole stack so the user lands on the deep screen with a working back button. For persistence across launches, make your route enum Codable and encode the path with NavigationPath's CodableRepresentation. This is the feature NavigationView never had.
func handle(_ url: URL) {
// myapp://product/42/reviews
guard url.host == "product",
let id = Int(url.pathComponents[safe: 1] ?? "") else { return }
router.path = NavigationPath()
router.path.append(Product(id: id))
if url.pathComponents.last == "reviews" {
router.path.append(Route.reviews(productID: id))
}
}Toolbars and titles
Inside a NavigationStack, attach .navigationTitle to name a screen and .toolbar to add bar buttons. Use ToolbarItem with a placement such as .topBarTrailing or .topBarLeading for precise positioning, and .navigationBarTitleDisplayMode to switch between large and inline titles. On iOS 26, the navigation bar adopts the Liquid Glass material and toolbar items can be grouped with ToolbarItemGroup so they share a glass background as the user scrolls.
DetailView()
.navigationTitle("Order Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Edit") { isEditing = true }
}
ToolbarItem(placement: .topBarLeading) {
Button("Close", role: .cancel) { dismiss() }
}
}Ship your SwiftUI app in 5 emails
A free 5-part course on the parts that actually stall launches: paywall, auth, onboarding, App Store review, and pricing. No fluff, unsubscribe anytime.
One router, every entry point
The Swift Kit centralizes routes in a typed Router so deep links, notifications, and widget taps all drive the same NavigationPath — no tangled bindings.
Rather have it done for you? I set up and ship your app from $499. See how
How to migrate from NavigationView to NavigationStack
Move an existing screen to the modern value-based navigation model in four steps.
- 1
Swap the container
Replace NavigationView with NavigationStack. Split-view layouts should move to NavigationSplitView instead.
NavigationStack { /* content */ } - 2
Convert links to values
Change NavigationLink(destination:) to NavigationLink(value:) carrying a Hashable value.
NavigationLink(value: item) { Text(item.name) } - 3
Register destinations
Add navigationDestination(for:) once per value type to map values to views.
.navigationDestination(for: Item.self) { DetailView(item: $0) } - 4
Add a path for control
If you need programmatic navigation, bind NavigationStack(path:) to a NavigationPath.
NavigationStack(path: $router.path) { ... }
Frequently Asked Questions
How do I navigate programmatically with NavigationStack in SwiftUI?
How does navigationDestination(for:) work in SwiftUI?
How do I pop to the root view in NavigationStack?
How do I restore a deep link into a SwiftUI NavigationStack?
Is NavigationView deprecated in favor of NavigationStack?
Keep exploring
Navigation you won't refactor later
The Swift Kit is a $99 one-time SwiftUI boilerplate with a typed router, auth, and paywalls already integrated — lifetime updates and a 14-day refund.
Get The Swift Kit — $99One-time purchase · Lifetime updates · 14-day refund