iOS 16 brought NavigationStack and deprecated NavigationView. iOS 17 really pushed the migration. I moved two of my iOS apps from NavigationView to NavigationStack and hit a few surprises along the way.
If you’re still on NavigationView or thinking about migrating, here are the seven meaningful differences you’ll run into.
1. NavigationStack is path-based
In the old NavigationView the navigation stack was implicit. You tapped a NavigationLink and SwiftUI handled it behind the scenes. Controlling the state was hard.
NavigationStack exposes an explicit path binding. It’s an array or a NavigationPath. You can manipulate navigation state from code now.
@State private var path = NavigationPath()
NavigationStack(path: $path) {
// ...
}That’s the biggest win. Programmatic navigation, deep linking, and state restoration finally work.
2. navigationDestination() gives you type-based routing
In the old system every NavigationLink declared its own destination. Now you route by type:
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}Push a User onto the path and the right destination opens automatically. Type-safe and refactor-friendly.
3. NavigationLink has a new syntax
Two forms:
Value-based (recommended):
NavigationLink(value: user) { UserRow(user: user) }Destination-based (old style):
NavigationLink { UserDetailView(user: user) } label: { UserRow(user: user) }The value-based form works with navigationDestination. The destination-based form still compiles, but you lose path tracking.
4. Programmatic navigation actually works
Under the old system, “login succeeded, take me to home” relied on tricks like NavigationLink(isActive:), with its bugs and glitches.
With NavigationStack:
@State private var path = NavigationPath()
func login() async {
let user = await authService.login()
path.append(user)
// Or deeper: path.append(HomeRoute.dashboard)
}Clean, predictable, testable.
5. Deep link handling is now native
From a universal link or URL scheme you just set the path:
func handleDeepLink(url: URL) {
if url.pathComponents.contains("user") {
let userId = url.lastPathComponent
path = NavigationPath([User(id: userId)])
}
}The old system needed three or four state bindings to do the same thing.
6. Backward compatibility to think about
Still supporting iOS 15 and below? You can’t use NavigationStack (iOS 16+). You have to dual-target:
if #available(iOS 16.0, *) {
NavigationStack { ContentView() }
} else {
NavigationView { ContentView() }
}Ugly but necessary. If your iOS 15 user share is below 5%, consider raising the minimum to iOS 16.
7. Split-view behaviour is different
NavigationView’s .columns style gave you iPad split view. NavigationStack is single-column, and for split view there’s NavigationSplitView:
NavigationSplitView {
SidebarView()
} content: {
ListView()
} detail: {
DetailView()
}On iPhone it behaves as a single column, on iPad it’s a three-column split. Migrating meant porting NavigationView’s adaptive behaviour over to NavigationSplitView.
My migration strategy
Across two apps I moved in this order:
Step 1: Global replace NavigationView with NavigationStack. It works right away, even without a path.
Step 2: Convert the most-used NavigationLinks to the value-based form. Add navigationDestination(for:).
Step 3: Refactor programmatic navigation to use the path. Auth flow, deep links, tab-to-detail flows.
Step 4: If you have an iPad layout, move it to NavigationSplitView.
Step 5: Test the edge cases. Back gesture, swipe, state restoration.
For a typical app it’s a one or two day job. For something with complex navigation it’s three to five days.
Bugs I hit
A few things to watch:
- iOS 16.0 bugs: NavigationStack had animation glitches on iOS 16.0 that were fixed in 16.1+. I bumped the minimum target to 16.1.
- Non-Hashable values in the path. NavigationPath requires Hashable. Custom types have to conform.
- Deeply nested navigation. Performance drops past five levels deep. Reconsider the pattern, a modal sheet might fit better.
Notes on NavigationSplitView
The iPad optimisation is significant. NavigationView’s adaptive behaviour was limited. NavigationSplitView gives you:
.balanced,.prominentDetail,.doubleColumnstyles- Built-in Dynamic Type and accessibility support
- Automatic state restoration
If you have an iPad build, migrating to NavigationSplitView is where you get those.
Takeaway
NavigationStack migration is a one or two day job. What you get: real programmatic navigation, deep link support, type-safe routing, testability. On any iOS 16+ target, make the move.
Ready to drop iOS 15 support? Don’t put it off. The new navigation mental model is more solid and SwiftUI will keep building on it.