SwiftUI Tutorial · iOS 16+ · iOS 26 Liquid Glass

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.

Last updated: 2026-07-17 11 min read By Ahmed Gagan, iOS Engineer
Quick Answer

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.

Replaces
The deprecated NavigationView (iOS 16+)
Value-driven push
NavigationLink(value:) + navigationDestination(for:)
Path control
NavigationStack(path:) with NavigationPath
Swift Kit support
The Swift Kit centralizes routes in a typed Router so deep links and push notifications share one path.

Why NavigationStack replaced NavigationView

NavigationView is deprecated because its navigation state was implicit and hard to control — you nested NavigationLinks and toggled isActive booleans that quickly became unmanageable. NavigationStack flips the model: you navigate by presenting values, and a single stack owns the history. This makes state restoration, deep linking, and unit-testable routing straightforward. If you are still on NavigationView, migrating is usually mechanical, and it unlocks the entire value-based API described below.

Before and after
// Deprecated
NavigationView {
    List(items) { item in
        NavigationLink(destination: DetailView(item: item)) {
            Text(item.name)
        }
    }
}

// Modern
NavigationStack {
    List(items) { item in
        NavigationLink(value: item) { Text(item.name) }
    }
    .navigationDestination(for: Item.self) { item in
        DetailView(item: item)
    }
}

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.

Multiple destination types on one stack
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()
        }
    }
}

Programmatic control with NavigationPath

For full programmatic navigation, bind a path with NavigationStack(path:). NavigationPath is a type-erased collection that can hold values of different Hashable types at once. Append to push a screen, call removeLast() to pop, and set it to an empty NavigationPath() to pop all the way to the root. Store this path in an observable Router object so any part of your app — a notification handler, a widget tap — can drive navigation without passing bindings everywhere.

A router that owns the path
@Observable
final class Router {
    var path = NavigationPath()
    func push(_ value: any Hashable) { path.append(value) }
    func popToRoot() { path = NavigationPath() }
}

struct RootView: View {
    @State private var router = Router()
    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: Product.self) { ProductDetail(product: $0) }
        }
        .environment(router)
    }
}

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.

Title and toolbar buttons
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.

Get The Swift Kit — $99

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. 1

    Swap the container

    Replace NavigationView with NavigationStack. Split-view layouts should move to NavigationSplitView instead.

    NavigationStack { /* content */ }
  2. 2

    Convert links to values

    Change NavigationLink(destination:) to NavigationLink(value:) carrying a Hashable value.

    NavigationLink(value: item) { Text(item.name) }
  3. 3

    Register destinations

    Add navigationDestination(for:) once per value type to map values to views.

    .navigationDestination(for: Item.self) { DetailView(item: $0) }
  4. 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?
Bind NavigationStack(path:) to a NavigationPath stored in state. Call path.append(value) to push and path.removeLast() to pop. Register the value type with navigationDestination(for:) so SwiftUI knows which view to render.
How does navigationDestination(for:) work in SwiftUI?
It maps a Hashable value type to a destination view. When a NavigationLink(value:) or a path append introduces a value of that type, SwiftUI builds the matching destination. You can register several destination types on one stack.
How do I pop to the root view in NavigationStack?
Assign an empty NavigationPath() to the bound path, which clears the entire stack and returns to the root. To pop a single level, call path.removeLast().
How do I restore a deep link into a SwiftUI NavigationStack?
Parse the incoming URL into your route values and append them to the path in order. SwiftUI pushes the whole stack, so the user lands deep with a working back button. Make routes Codable to persist the path across launches.
Is NavigationView deprecated in favor of NavigationStack?
Yes. NavigationView is deprecated as of iOS 16. Use NavigationStack for single-column push navigation and NavigationSplitView for multi-column layouts on iPad and Mac.

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 — $99

One-time purchase · Lifetime updates · 14-day refund