Why this happens
SwiftUI’s rendering model confuses many developers because it tracks dependencies, not ownership. A view only becomes dependent on a @State value when its body directly reads that value. Simply declaring @State in a view does not guarantee the view will re-render when that state changes.
In the example from the question, ParentView declares @State var counter but never reads counter inside its body. The value is passed as a Binding ($counter) to ChildView inside a .sheet. Because ParentView‘s body doesn’t access counter, SwiftUI has no reason to re-evaluate ParentView.body when counter changes. The child view, however, reads the binding directly, so it updates independently.
This behavior is intentional. SwiftUI builds a dependency graph at render time. If a view’s body doesn’t touch a piece of state, that state is not a dependency, and changes to it are not scheduled to invalidate the view. The same principle applies when passing state to view modifiers like .sheet(isPresented:) — the modifier manages its own dependency on the binding.
@Published behaves differently because it lives on an ObservableObject (a reference type). SwiftUI subscribes to the object’s objectWillChange publisher. Any view that holds an @ObservedObject or @StateObject reference to that object will re-render when any @Published property changes, unless you use @ObservedObject with a custom objectWillChange or move to the observation framework in iOS 17+.
How to fix it
1. Read the state in the parent body if you need the parent to update
If ParentView should react to counter changes, read the value somewhere in body. Even a harmless read like _ = counter or using it in a Text forces the dependency.
struct ParentView: View {
@State private var counter = 0
var body: some View {
let _ = Self._printChanges()
VStack {
// Reading counter here creates the dependency
Text("Parent sees: \(counter)")
.font(.caption)
.foregroundStyle(.secondary)
Button("Increment from Parent") { counter += 1 }
Button("Open Sheet") { showSheet = true }
}
.sheet(isPresented: $showSheet) {
ChildView(counter: $counter)
}
}
}
Now ParentView.body re-evaluates when counter changes.
2. Use the Observation framework (iOS 17+) for fine-grained updates
If you want a parent to update only when specific properties change, migrate from ObservableObject/@Published to @Observable:
@Observable
final class CounterModel {
var value = 0
}
struct ParentView: View {
@State private var model = CounterModel()
var body: some View {
// Only re-renders when model.value is read and changes
Text("Value: \(model.value)")
ChildView(model: model)
}
}
struct ChildView: View {
@Bindable var model: CounterModel
var body: some View {
Button("Increment") { model.value += 1 }
}
}
With @Observable, SwiftUI tracks exactly which properties each view reads.
3. Understand that Binding passes write access without read dependency
Passing $state to a child gives the child a Binding<Value>. The child reads and writes through that binding. The parent only re-renders if it reads the state. This is not a bug — it’s how SwiftUI avoids unnecessary work.
If you need the parent to know about a change initiated by the child, you have two options:
– Read the state in the parent (fix 1)
– Use a callback/closure from the child to notify the parent explicitly
struct ChildView: View {
@Binding var counter: Int
var onChange: (Int) -> Void = { _ in }
var body: some View {
Button("Increment") {
counter += 1
onChange(counter)
}
}
}
// In ParentView:
ChildView(counter: $counter) { newValue in
// Parent can react here without reading counter in body
print("Child changed counter to \(newValue)")
}
How to prevent it
- Read every
@Stateyou care about in the view’sbody. Even alet _ = stateforces the dependency. - Use
@Observable(iOS 17+) for reference-type models to get automatic fine-grained tracking. - Avoid
@PublishedonObservableObjectwhen you need property-level granularity; it invalidates all observing views on any change. - Use
Self._printChanges()during development to see exactly which views re-render and why. - Prefer value types (
struct) with@Statefor view-local state; they follow the read-dependency rule predictably.
FAQ
Why does my parent view not update when a child changes a @State passed as Binding?
Because the parent’s body never reads that @State property. SwiftUI only schedules a re-render for views that directly depend on the value. The child updates because it reads the binding.
Is this different from @Published on ObservableObject?
Yes. @Published triggers objectWillChange on the whole object. Any view holding @ObservedObject or @StateObject re-renders when any @Published property changes, regardless of which property the view actually uses.
How can I make a parent react to a child’s binding change without reading the state in body?
Pass a callback closure from the parent to the child. The child calls the closure after mutating the binding. The parent can then perform side effects or update other state without needing to read the bound value in its body.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
