NavigationStack and destinationsswift-6.4/ios-26
Lesson 1 / 4
Unit 9 · Navigation & app structure

NavigationStack and destinations

Note

Unit 9 · Navigation & app structure. NavigationStack is the modern replacement for the deprecated NavigationView. Everything in this unit — programmatic paths, split views, coordinators — builds on the value-based model introduced here.

NavigationStack manages a stack of screens with the expected push/pop behavior. The modern approach is value-based navigation: you present a value, and the stack maps that value's type to a destination view. This decouples "the user tapped this" from "here is the screen," which is what makes deep linking and programmatic control (Lesson 9.2) possible.

The basic stack

Wrap your root in a NavigationStack and give it a title:

NavigationStack {
    List(products) { product in
        NavigationLink(product.name, value: product)
    }
    .navigationTitle("Products")
}

NavigationLink(_:value:) does not name a destination view. It says "push this value when tapped." The stack still needs to know what screen a Product maps to — that is the next piece.

navigationDestination(for:)

Register a destination per value type with .navigationDestination(for:). It runs a builder that turns a pushed value into a view:

NavigationStack {
    List(products) { product in
        NavigationLink(product.name, value: product)
    }
    .navigationTitle("Products")
    .navigationDestination(for: Product.self) { product in
        ProductDetail(product: product)
    }
}

Now any Product pushed anywhere in this stack — from this list, from a search result, from a deep link — lands on ProductDetail. The value type must be Hashable so the stack can identify and diff entries. You can register several destinations for several types:

.navigationDestination(for: Product.self)  { ProductDetail(product: $0) }
.navigationDestination(for: Category.self) { CategoryScreen(category: $0) }
Tip

Separate the trigger from the destination. The link carries a value; the navigationDestination decides the screen. That split is the whole point: a Product shown as a row, surfaced by search, or arriving from a URL all reach the same detail screen through one registration. Old-style NavigationLink(destination:) hard-wired the screen into the tap site — reserve it for trivial, one-off pushes only.

Titles and bars

.navigationTitle sets the screen title; toolbar items ride the same bar, which on iOS 26 is Liquid Glass automatically (Unit 7):

ProductDetail(product: product)
    .navigationTitle(product.name)
    .navigationBarTitleDisplayMode(.inline)
    .toolbar {
        ToolbarItem(placement: .primaryAction) {
            Button("Share", systemImage: "square.and.arrow.up") { }
        }
    }

Apply .navigationTitle on the destination content, not the stack — each pushed screen sets its own title, and the back button label is derived from the previous screen's title automatically.

Coming from older Swift

NavigationStack replaces NavigationView (now deprecated) and the UIKit UINavigationController push/pop model. The big shift: instead of pushViewController(_:animated:) with a fully-built view controller, you push a value and a single navigationDestination builds the screen. .navigationTitle replaces setting navigationItem.title on each controller.

Knowledge check

Q: What does NavigationLink(_:value:) push, and what turns that into a screen? It pushes a value, not a view. A matching .navigationDestination(for:) registered on the stack maps that value's type to the destination view that gets built.

Q: Why is value-based navigation better than NavigationLink(destination:)? It decouples the tap site from the screen. One navigationDestination serves every place that pushes that value type — a list row, a search result, a deep link — which is what makes programmatic navigation and deep linking possible.