NavigationStack in SwiftUI: Programmatic Navigation with NavigationPath

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.