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.
One Link, Driven by a Binding
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.