How to use NavigationStack in SwiftUI | SwiftUI Bootcamp #62
NavigationStack is the modern replacement for NavigationView introduced in iOS 16, and it solves a real problem: your entire navigation path is now a value you can read, write, and restore — which makes deep linking, state restoration, and programmatic navigation finally straightforward.
What You'll Learn
- How
NavigationStackmanages navigation as a typed array (path) - How to declare destinations with
.navigationDestination(for:destination:)for different data types - How to push multiple screens at once programmatically using
path.append(contentsOf:)
Mental Model
Think of NavigationStack as a stack of plates in a cafeteria. Each time you navigate forward, you add a plate to the top. The back button removes the top plate. The path array is a written record of every plate currently stacked — "first plate: Coconut, second plate: Watermelon". Because it's just an array, you can add five plates at once, inspect the current stack at any time, or clear the stack entirely to go back to the root screen.
The old NavigationView hid this stack from you entirely, making it impossible to manipulate programmatically. NavigationStack exposes the stack as data you own.
Detailed Explanation
NavigationStack was introduced in iOS 16 to replace NavigationView. The key improvement is the path parameter: you pass a binding to an array of Hashable values, and SwiftUI uses that array as the definitive record of which screens are on screen. Push to the array to go deeper; pop from the array to go back; clear the array to return to root.
The .navigationDestination(for:destination:) modifier is how you tell SwiftUI what view to show for each type of value. You can register multiple destinations for different types — one closure handles Int values, another handles String values. SwiftUI matches each item in path to the right destination by its type, which is why the path values must be Hashable.
NavigationLink(value:) is the preferred link style with NavigationStack. Instead of directly embedding the destination view, you pass a value that gets appended to the path. This is more efficient because SwiftUI only instantiates the destination view when the user actually navigates to it — the print("INIT SCREEN: \(value)") in the sample proves this.
When to use NavigationStack: any app that runs on iOS 16+ and needs stack-based navigation. When to avoid it: if you need to support iOS 15 or earlier, stick with NavigationView. Do not mix NavigationStack and NavigationView in the same navigation hierarchy — they are incompatible.
Code Structure
NavigationStackBootcamp.swift contains the root screen (NavigationStackBootcamp) and a detail screen (MySecondScreen). The root demonstrates three things simultaneously: programmatic multi-push via a button, String-typed navigation links, and Int-typed navigation links — all resolved by two separate .navigationDestination modifiers.
Complete Code
NavigationStackBootcamp.swift
import SwiftUI
struct NavigationStackBootcamp: View {
let fruits = ["Apple", "Orange", "Banana"]
// The path array IS the navigation stack — its contents determine which screens are shown
@State private var stackPath: [String] = []
var body: some View {
NavigationStack(path: $stackPath) { // Bind the stack to our path array
ScrollView {
VStack(spacing: 40) {
Button("Super segue!") {
// Appending multiple items pushes all three screens at once
stackPath.append(contentsOf: [
"Coconut", "Watermelon", "Mango"
])
}
ForEach(fruits, id: \.self) { fruit in
NavigationLink(value: fruit) { // Appends fruit string to path on tap
Text(fruit)
}
}
ForEach(0..<10) { x in
NavigationLink(value: x) { // Appends an Int to path on tap
Text("Click me: \(x)")
}
}
}
}
.navigationTitle("Nav Bootcamp")
.navigationDestination(for: Int.self) { value in // Handles Int items in the path
MySecondScreen(value: value)
}
.navigationDestination(for: String.self) { value in // Handles String items in the path
Text("ANOTHER SCREEN: \(value)")
}
}
}
}
struct MySecondScreen: View {
let value: Int
init(value: Int) {
self.value = value
print("INIT SCREEN: \(value)") // Proves the view is only created when navigated to
}
var body: some View {
Text("Screen \(value)")
}
}
struct NavigationStackBootcamp_Previews: PreviewProvider {
static var previews: some View {
NavigationStackBootcamp()
}
}Code Walkthrough
@State private var stackPath: [String] = []— This array is the single source of truth for navigation. An empty array means the root screen is showing. Each element pushes one screen onto the stack. In real apps you would useNavigationPath(a type-erased container) when the path contains mixed types; here a[String]array works because only strings are pushed directly.NavigationStack(path: $stackPath)— Passing$stackPathwires the navigation system to your state. When SwiftUI's back button is tapped, it removes the last item fromstackPathautomatically. You can also clearstackPath = []from anywhere to pop all the way to root.stackPath.append(contentsOf: [...])— This is the "super segue" — pushing three values at once means three screens are pushed simultaneously. The user lands on the third screen with a back button that will take them through all three. This is how deep linking works: you reconstruct the path array from a URL and the user sees exactly the right screen.NavigationLink(value: fruit)— This is the value-basedNavigationLink. It does not embed a destination view — it just says "when tapped, append this value to the path". The actual view is determined by.navigationDestination..navigationDestination(for: Int.self)— SwiftUI looks at each item added topath, checks its type, and calls the matchingnavigationDestinationclosure. Having one per type keeps destination logic organized and avoids giant switch statements.print("INIT SCREEN: \(value)")— A deliberate diagnostic to show thatMySecondScreenis only initialized when it actually appears. The oldNavigationLinkwith an inline destination would initialize every destination view eagerly, which is why the new pattern is much more performant in long lists.
Common Mistakes
Mistake: Mixing NavigationStack with the old NavigationView
If you wrap a NavigationView inside a NavigationStack (or vice versa), the back button behavior breaks and the navigation bar may appear doubled. Pick one and use it consistently. Prefer NavigationStack for iOS 16+ targets.
Mistake: Putting .navigationDestination on the wrong view.navigationDestination must be applied inside the NavigationStack, but ideally on the root content view inside it — not on views that are themselves pushed as destinations. If you attach it to a child screen inside the stack, navigation may not work reliably.
Mistake: Using NavigationLink(destination:) with inline views in a List
The old NavigationLink(destination:) pattern that embeds the full destination view is legal but inefficient in NavigationStack. All destination views are created upfront. Use NavigationLink(value:) + .navigationDestination to keep initialization lazy.
Key Takeaways
NavigationStackexposes navigation as apatharray you fully own — read it, write it, clear it- Register view destinations per type with
.navigationDestination(for:)rather than embedding views directly in links - Programmatic deep linking is as simple as setting
pathto an array representing the desired screen hierarchy
Last updated: June 27, 2026