SwiftUI’s First-Toggle Re-Render: Documenting @Binding Location Box Behavior
When profiling SwiftUI view trees with Self._printChanges(), child views receiving projected bindings ($) from an ObservableObject (@StateObject or @ObservedObject) exhibit a unique lifecycle pattern on their first parent state update.
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 with two models: one using ObservableObject (CombineModel) and one using Swift 5.9’s @Observable macro (ObservableModel).
Swift
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 {
Form {
Toggle("isOn \(toggle.description)", isOn: $toggle)
BindingTestA(counter: $model1.counter)
BindingTestB(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)")
}
}
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:
Swift
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, both child views execute their body properties:
BindingTestA(@StateObject/@ObservedObject):bodyexecuted.LocationBoxAddress:0x10be379c0
BindingTestB(@Observable):bodyexecuted.
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(@Observable):Self._printChanges()output: (No output)bodyskipped.
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(@Observable):Self._printChanges()output: (No output)bodyskipped.
Fact Summary Table
| Render Event | BindingTestA (@StateObject / @ObservedObject) | BindingTestA LocationBox Address | BindingTestB (@Observable) |
| Initial Render | body called | 0x10be379c0 (Address A) | body called |
| 1st Toggle | body called (_counter changed) | 0x10bfe1680 (Address B) | body skipped |
| 2nd Toggle | body skipped | 0x10bfe1680 (Address B) | body skipped |
| 3rd+ Toggle | body skipped | 0x10bfe1680 (Address B) | body skipped |
Empirical Observations
- Both
@StateObjectand@ObservedObjectshare this behavior: Projecting a property binding ($) from either property wrapper type produces aLocationBox<...ScopedLocation>that changes memory address between the initial render and the first parent state update. - The re-render occurs exactly once: On the first parent state change,
Self._printChanges()reports_counter changedfor theObservableObjectbinding, executing the child view’s body a second time. - Subsequent state updates stabilize: From the second parent update onward, the
LocationBoxaddress remains identical across passes, and SwiftUI prunes/skips the child view evaluation. @Observabledoes not exhibit the first-toggle re-render: Projected bindings derived from@Observablemodel properties skip child view re-evaluation starting from the very first parent state change.