NavigationStack and destinations
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) }
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.
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.