Introduction
SwiftUI navigation driven by @Binding values sometimes fails to update when the binding changes. This is typically caused by the binding not being properly connected to a @State source of truth, view identity issues where SwiftUI reuses a view instead of re-rendering, or the use of deprecated navigation APIs that do not respond to binding changes correctly.
Symptoms
- Setting
isActive = truedoes not push the navigation destination NavigationLinkwith binding does not respond to state changes- Navigation works once but not on subsequent attempts
- Programmatic navigation via
NavigationPathdoes not update the UI - Works in iOS 16+
NavigationStackbut not with deprecatedNavigationView
Example problem: ```swift struct ContentView: View { @State private var isActive = false
var body: some View { NavigationView { VStack { NavigationLink("Go", isActive: $isActive) { DetailView() } Button("Navigate") { isActive = true // Does not trigger navigation } } } } } ```
Common Causes
- Using deprecated
NavigationViewwithNavigationLink(isActive:)in iOS 16+ - Binding created from a computed property instead of
@State - View identity does not change, so SwiftUI skips the update
- Multiple
NavigationLinkwith the same destination compete NavigationPathchanges but theNavigationStackdoes not observe them
Step-by-Step Fix
- 1.Use NavigationStack with NavigationPath (iOS 16+):
- 2.```swift
- 3.struct ContentView: View {
- 4.@State private var path = NavigationPath()
var body: some View { NavigationStack(path: $path) { List { Button("Go to Detail") { path.append("detail") } Button("Go to Settings") { path.append("settings") } } .navigationDestination(for: String.self) { destination in switch destination { case "detail": DetailView() case "settings": SettingsView() default: EmptyView() } } } } } ```
- 1.Use Hashable types for NavigationPath:
- 2.```swift
- 3.enum Route: Hashable {
- 4.case detail(id: UUID)
- 5.case settings
- 6.case profile(User)
- 7.}
struct ContentView: View { @State private var path = NavigationPath<Route>()
var body: some View { NavigationStack(path: $path) { Button("View User") { path.append(.profile(User(id: UUID(), name: "John"))) } .navigationDestination(for: Route.self) { route in switch route { case .detail(let id): DetailView(id: id) case .settings: SettingsView() case .profile(let user): ProfileView(user: user) } } } } } ```
- 1.Fix NavigationLink isActive binding:
- 2.```swift
- 3.// If you must use NavigationLink with isActive:
- 4.struct ContentView: View {
- 5.@State private var selectedTab: String?
var body: some View { NavigationStack { List { NavigationLink("Detail", value: "detail") NavigationLink("Settings", value: "settings") } .navigationDestination(for: String.self) { tab in if tab == "detail" { DetailView() } else { SettingsView() } } } } } ```
- 1.Force view update with id modifier:
- 2.```swift
- 3.// When binding changes but view does not update:
- 4.struct ContentView: View {
- 5.@State private var showDetail = false
var body: some View { NavigationStack { DetailView() .id(showDetail) // Forces recreation when showDetail changes .sheet(isPresented: $showDetail) { DetailView() } } } } ```
Prevention
- Use
NavigationStackinstead of deprecatedNavigationViewon iOS 16+ - Use
NavigationPathwithHashabletypes for programmatic navigation - Ensure bindings always connect to
@State,@StateObject, or@Environment - Add
.id()modifier when view identity needs to change with state - Test navigation on the minimum supported iOS version
- Avoid deeply nested navigation state; keep it at the root view level