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 = true does not push the navigation destination
  • NavigationLink with binding does not respond to state changes
  • Navigation works once but not on subsequent attempts
  • Programmatic navigation via NavigationPath does not update the UI
  • Works in iOS 16+ NavigationStack but not with deprecated NavigationView

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 NavigationView with NavigationLink(isActive:) in iOS 16+
  • Binding created from a computed property instead of @State
  • View identity does not change, so SwiftUI skips the update
  • Multiple NavigationLink with the same destination compete
  • NavigationPath changes but the NavigationStack does not observe them

Step-by-Step Fix

  1. 1.Use NavigationStack with NavigationPath (iOS 16+):
  2. 2.```swift
  3. 3.struct ContentView: View {
  4. 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. 1.Use Hashable types for NavigationPath:
  2. 2.```swift
  3. 3.enum Route: Hashable {
  4. 4.case detail(id: UUID)
  5. 5.case settings
  6. 6.case profile(User)
  7. 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. 1.Fix NavigationLink isActive binding:
  2. 2.```swift
  3. 3.// If you must use NavigationLink with isActive:
  4. 4.struct ContentView: View {
  5. 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. 1.Force view update with id modifier:
  2. 2.```swift
  3. 3.// When binding changes but view does not update:
  4. 4.struct ContentView: View {
  5. 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 NavigationStack instead of deprecated NavigationView on iOS 16+
  • Use NavigationPath with Hashable types 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