Skip to content

Lazy Navigation (SwiftUI)

A SwiftUI guide for coding agents. Also covers navigationlink performance, too many navigation links, navigate on selection binding, hidden navigationlink background, programmatic push optional value.

A screen with hundreds of NavigationLinks builds every link and destination up front, and SwiftUI re-diffs them all on each state change; a two-year calendar means about 1,460 extra views. The fix is one hidden link driven by an optional selection, built only when tapped.

A failable NavigationLink initializer takes a Binding<Value?>. No value, no link. When a value arrives, the link builds its destination from it and activates; deactivating clears the binding back to nil.

extension NavigationLink where Label == EmptyView {
  init?<Value>(
    _ binding: Binding<Value?>,
    @ViewBuilder destination: (Value) -> Destination
  ) {
    guard let value = binding.wrappedValue else {
      return nil
    }

    let isActive = Binding(
      get: { true },
      set: { newValue in if !newValue { binding.wrappedValue = nil } }
    )

    self.init(destination: destination(value), isActive: isActive, label: EmptyView.init)
  }
}

The EmptyView label keeps the link invisible, so it can hide in a background without affecting layout.

extension View {
  @ViewBuilder
  func navigate<Value, Destination: View>(
    using binding: Binding<Value?>,
    @ViewBuilder destination: (Value) -> Destination
  ) -> some View {
    background(NavigationLink(binding, destination: destination))
  }
}

Using It

Rows become plain buttons that set the selection; the single navigate modifier does the pushing.

@State private var selectedDate: Date?

ScrollView {
  CalendarView(interval: interval) { date in
    Button(action: { selectedDate = date }) {
      DateView(date: date)
    }
  }
}
.navigate(using: $selectedDate, destination: makeDestination)

Two wins: SwiftUI recalculates far fewer views on state changes, and the destination is decided lazily, so makeDestination can branch on the selected value at tap time.

This predates NavigationStack. On iOS 16+, navigationDestination(item:destination:) is the built-in equivalent of this pattern; the value-driven design carries over directly.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-swiftui-guide({ topic: "swiftui-lazy-navigation" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems