How does navigation work in modern SwiftUI (NavigationStack) vs the old NavigationView?
NavigationView pushed screens through hidden per-link state that broke deep linking and programmatic navigation; NavigationStack replaced it with an explicit, observable path you own.
LEARN BY DOINGPut your intuition to work3 steps
How does navigation work in modern SwiftUI (NavigationStack) vs the old NavigationView?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.Ask a senior iOS candidate about navigation and you're really testing something narrower: have they shipped an app through the NavigationView era and survived the migration, or have they only ever known NavigationStack? Both answers can be fine, but the candidate needs to know why Apple replaced the API in iOS 16 rather than just reciting that it happened. The short version: NavigationView managed navigation state implicitly, tied to view identity, and that implicit state is exactly what made deep linking, programmatic push/pop, and state restoration so painful.
#How NavigationView worked
NavigationView pushed a new screen whenever a NavigationLink(destination:) inside it became active — normally because the user tapped it, which flipped a hidden boolean owned by the link itself. Programmatic navigation meant reaching for NavigationLink(destination:isActive:) or the tag/selection initializer, binding a Bool or an optional value to a link that might not even be visible on screen yet:
struct OldStyleListView: View {@State private var showDetail = false@State private var selectedItem: Item?var body: some View {NavigationView {List(items) { item inButton(item.title) {selectedItem = itemshowDetail = true}}.background(NavigationLink(destination: DetailView(item: selectedItem),isActive: $showDetail) { EmptyView() }.hidden())}}}
That "hidden NavigationLink glued to the background" pattern was not an accident of a bad codebase — it was the recommended workaround, because NavigationView had no concept of a navigation stack you could inspect or mutate from outside. It also shipped a well-known performance trap: a List full of NavigationLink(destination:) rows could construct every destination view eagerly instead of lazily, because the destination closure got evaluated well before the user ever tapped a row. Teams worked around it with a hand-rolled LazyView wrapper that deferred body until the link was actually activated.
#How NavigationStack works
NavigationStack inverts the model: the stack's contents are an explicit, observable value you own — either a typed array or a NavigationPath — and pushing or popping is just mutating that value.
enum Route: Hashable {case detail(Item)case settings}struct ModernListView: View {@State private var path: [Route] = []var body: some View {NavigationStack(path: $path) {List(items) { item inNavigationLink(item.title, value: Route.detail(item))}.navigationDestination(for: Route.self) { route inswitch route {case .detail(let item):DetailView(item: item)case .settings:SettingsView()}}.navigationTitle("Items")}}}
NavigationLink(value:) no longer knows or cares what it pushes — it just appends a value to the path when tapped. The .navigationDestination(for:) modifier is a lookup table: "when a value of this type shows up on the path, build this view." Programmatic navigation is now just array mutation: path.append(.settings) pushes, path.removeLast() pops, path = [] pops to root. Because path is a normal piece of state, it's trivial to persist, replay, or drive from a view model instead of from view-local @State.
For heterogeneous route types you don't want to unify into one enum, NavigationPath is the type-erased container built for exactly that — it accepts any Hashable value, and its .codable representation lets you persist an entire navigation stack across app launches, something NavigationView never offered a supported way to do.
struct RootView: View {@State private var path = NavigationPath()var body: some View {NavigationStack(path: $path) {HomeView().navigationDestination(for: Item.self) { DetailView(item: $0) }.navigationDestination(for: Settings.self) { SettingsView() }}}}
For multi-column layouts on iPad and Mac, NavigationSplitView replaces the old NavigationView "automatically becomes a split view on regular width" behavior with an explicit sidebar/content/detail API, so the responsive behavior is something you configure rather than something that happens to you.
🧠 NavigationStack vs NavigationView
0/2 · 0/2 answered1. What is the core architectural difference between NavigationStack and the old NavigationView?
2. How do you push a new screen programmatically with NavigationStack, given `@State private var path: [Route] = []`?