SwiftUI’s First-Toggle Re-Render: Documenting @Binding Location Box Behavior
When profiling SwiftUI view trees with Self._printChanges(), child views receiving projected bindings ($) through intermediate wrappers like @StateObject, @ObservedObject, or @Bindable exhibit a unique lifecycle pattern on their first parent state update.
Only projected bindings created directly off @State bypass this first-toggle re-render.
Here is a minimal reproducible test case documenting the exact behavior, memory addresses, and diffing output across consecutive render passes.
The Minimal Test Setup
Consider a view hierarchy testing three binding sources:
- Projection via
@StateObject($model1.counter) - Projection via
@Bindable($bindableModel.counter) - Direct projection via
@State($model2.counter)
import SwiftUI
class CombineModel: ObservableObject {
@Published var counter = 0
}
@Observable
class ObservableModel {
var counter = 0
}
struct ContentView: View {
@StateObject var model1 = CombineModel()
@State var model2 = ObservableModel()
@State var toggle = false
var body: some View {
@Bindable var bindableModel = model2
Form {
Toggle("isOn \(toggle.description)", isOn: $toggle)
BindingTestA(counter: $model1.counter)
BindingTestB(counter: $bindableModel.counter)
BindingTestC(counter: $model2.counter)
}
}
}
struct BindingTestA: View {
@Binding var counter: Int
var body: some View {
let _ = Self._printChanges()
Text("Value A: \(counter)")
}
}
struct BindingTestB: View {
@Binding var counter: Int
var body: some View {
let _ = Self._printChanges()
Text("Value B: \(counter)")
}
}
struct BindingTestC: View {
@Binding var counter: Int
var body: some View {
let _ = Self._printChanges()
Text("Value C: \(counter)")
}
}
To inspect the internal pointer identity of the binding, we can use Swift’s Mirror API to log the underlying LocationBoxmemory address inside the child views:
func printBindingAddress<T>(_ binding: Binding<T>, label: String) {
let mirror = Mirror(reflecting: binding)
if let box = mirror.children.first(where: { $0.label == "location" })?.value as? AnyObject {
let address = Unmanaged.passUnretained(box).toOpaque()
print("📍 [\(label)] LocationBox Address: \(address)")
}
}
The Observed Execution Log
Here is the exact trace of Self._printChanges() and memory addresses across three consecutive UI events:
Pass 1: Initial Render (onAppear)
When the application first loads, all three child views execute their body properties:
BindingTestA(@StateObject/@ObservedObject):bodyexecuted.LocationBoxAddress:0x10be379c0
BindingTestB(@Bindablewith@Observable):bodyexecuted.LocationBoxAddress:0x10be38120
BindingTestC(Direct@Statewith@Observable):bodyexecuted.LocationBoxAddress:0x10bfe2100
Pass 2: First Parent State Change (Toggle false $\rightarrow$ true)
When the user taps the toggle for the first time, ContentView re-evaluates its body.
BindingTestA(@StateObject/@ObservedObject):Self._printChanges()output:BindingTestA: _counter changed.bodyexecuted.LocationBoxAddress:0x10bfe1680(Address changed)
BindingTestB(@Bindablewith@Observable):Self._printChanges()output:BindingTestB: _counter changed.bodyexecuted.LocationBoxAddress:0x10bfe1940(Address changed)
BindingTestC(Direct@Statewith@Observable):Self._printChanges()output: (No output)bodyskipped.LocationBoxAddress:0x10bfe2100(Address unchanged)
Pass 3: Second Parent State Change (Toggle true $\rightarrow$ false)
When the user taps the toggle a second time, ContentView re-evaluates its body again.
BindingTestA(@StateObject/@ObservedObject):Self._printChanges()output: (No output)bodyskipped.LocationBoxAddress:0x10bfe1680(Address unchanged)
BindingTestB(@Bindablewith@Observable):Self._printChanges()output: (No output)bodyskipped.LocationBoxAddress:0x10bfe1940(Address unchanged)
BindingTestC(Direct@Statewith@Observable):Self._printChanges()output: (No output)bodyskipped.LocationBoxAddress:0x10bfe2100(Address unchanged)
Fact Summary Table
| Render Event | BindingTestA (@StateObject / @ObservedObject) | BindingTestB (@Bindable) | BindingTestC (Direct @State) |
| Initial Render | body called (0x10be379c0) | body called (0x10be38120) | body called (0x10bfe2100) |
| 1st Toggle | body called (0x10bfe1680 – Address changed) | body called (0x10bfe1940 – Address changed) | body skipped(0x10bfe2100 – Unchanged) |
| 2nd Toggle | body skipped (0x10bfe1680) | body skipped (0x10bfe1940) | body skipped(0x10bfe2100) |
| 3rd+ Toggle | body skipped (0x10bfe1680) | body skipped (0x10bfe1940) | body skipped(0x10bfe2100) |
Empirical Observations
@Bindableexhibits the exact same first-toggle re-render as@StateObjectand@ObservedObject: Passing an@Observableinstance through@Bindableproduces aLocationBoxwhose memory address changes between initial render and the first parent state update, triggering a child view re-evaluation.- The re-render occurs exactly once: On the first parent state change,
Self._printChanges()reports_counter changedfor both@StateObjectand@Bindableprojections. - Subsequent state updates stabilize: From the second parent update onward, the
LocationBoxaddress remains identical across passes for all projection types, and child view evaluations are skipped. - Direct
@Stateprojections do not exhibit the first-toggle re-render: Creating a binding directly from an@Stateproperty ($model2.counter) maintains a stableLocationBoxmemory address starting fromonAppear, skipping the child view’sbodyon the very first toggle.