Richer input and navigation
Navigate with NavigationStack
Learn how NavigationStack replaces NavigationView, pushes screens from data with navigationDestination, and binds the path to state for programmatic navigation.
NavigationView shows up in older SwiftUI tutorials, including the Picker lesson in this course. Apple deprecated it. New code should use NavigationStack for one-column navigation, the kind where screens push in from the right and a back button takes you home.
The change is not cosmetic. NavigationView hid its navigation state. You could not inspect it, restore it, or drive it from code without hacks. NavigationStack turns that state into a plain value your app can own.
Destinations as data
The simplest stack manages its own navigation. You describe each destination as a piece of data, and tell the stack how to turn that data into a screen:
NavigationStack {
List(notes) { note in
NavigationLink(note.title, value: note.id)
}
.navigationDestination(for: UUID.self) { id in
NoteDetail(id: id)
}
}
Tapping a link pushes its value onto the navigation path. The navigationDestination(for:) modifier matches values by type: every UUID pushed on this stack becomes a NoteDetail.
Notice the split. The link says what was selected. The destination modifier says how to present it. Links stay tiny, and all the destinations live in one place. That’s what makes the API easy to compose.
Binding the path to state
When the app needs to navigate from code, bind the path to a @State array:
struct NotesRoot: View {
@State private var path: [UUID] = []
var body: some View {
NavigationStack(path: $path) {
List(notes) { note in
NavigationLink(note.title, value: note.id)
}
.navigationDestination(for: UUID.self) { id in
NoteDetail(id: id)
}
}
}
}
Now navigation is just data. Appending an id pushes a screen: path.append(note.id). Setting path = [] pops back to the root. A deep link handler can build the whole array in one assignment, and the stack renders the full hierarchy, back button included.
If you need to push values of different types onto the same stack, use NavigationPath instead of a typed array. It accepts any Hashable value.
Push identifiers, not objects
Push stable, lightweight identifiers instead of whole model objects.
A common mistake is pushing a full Note struct into the path. It works at first. But the pushed copy goes stale the moment the model changes, and restoring state or handling a deep link means rebuilding entire objects instead of writing an id. Let the destination view look up the current model from the identifier it receives.
Try this on your own project: replace one NavigationView with NavigationStack, move the destination into a navigationDestination(for:) modifier, and add a button that resets path to []. You’ll have a working “back to top” button in a couple of minutes.
One pointer for later: on iPad and Mac, apps with a sidebar and a detail area should use NavigationSplitView instead. It manages the columns, and its detail column can still hold a NavigationStack of its own.
Lesson completed