NavigationStack, introduced in iOS 16, replaced the older NavigationView for pushing and popping screens. The main thing it adds is programmatic control: instead of only reacting to a user tapping a NavigationLink, your code can drive navigation directly by manipulating a path value.
Basic Setup
NavigationStack {
List(users) { user in
NavigationLink(user.name, value: user)
}
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
}
Notice NavigationLink(value:) instead of the older pattern of embedding the destination view directly inside the link. The destination is now declared separately with .navigationDestination(for:), keyed by the value’s type — this decoupling is what makes programmatic navigation possible.
Programmatic Navigation with NavigationPath
Bind the stack to a NavigationPath, and you can push or pop screens from anywhere — a button action, a deep link handler, the result of a network call — without needing a NavigationLink in the view at all:
@Observable
class Router {
var path = NavigationPath()
func goToUser(_ user: User) {
path.append(user)
}
func popToRoot() {
path.removeLast(path.count)
}
}
struct RootView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: User.self) { user in
UserDetailView(user: user)
}
}
.environment(router)
}
}
Any view that can reach router — via @Environment(Router.self), since it was injected into the environment — can now push a new screen by calling router.goToUser(someUser), with no direct reference to the NavigationStack itself.
Pushing Multiple Destination Types
navigationDestination(for:) can be declared multiple times for different types on the same stack, and NavigationPath can hold a heterogeneous mix of them:
.navigationDestination(for: User.self) { user in UserDetailView(user: user) }
.navigationDestination(for: Post.self) { post in PostDetailView(post: post) }
Why This Replaced NavigationView
NavigationView had no equivalent to NavigationPath — deep linking and programmatic multi-level navigation required awkward workarounds with chained, conditionally-active NavigationLinks. If your minimum deployment target is iOS 16 or later, there’s no reason to use NavigationView for new code.