A view’s init runs on every parent update. When does the initial value of @State or @StateObject actually get created?

8.10.1 · L3 · Interview

Predict first

How many times are the views and their models created?

@Observable final class Driver { var tick = 0 }
let driver = Driver()
nonisolated(unsafe) var made: [String: Int] = [:]
func bump(_ k: String) { made[k, default: 0] += 1 }
final class Plain { init() { bump("Plain.init") } }
final class OO: ObservableObject { init() { bump("OO.init") } }

struct A: View {
    @State private var model = Plain()
    init() { bump("A.init") }
    var body: some View { Text("a") }
}
struct B: View {
    @StateObject private var model = OO()
    init() { bump("B.init") }
    var body: some View { Text("b") }
}
struct Top: View {
    var body: some View { let _ = driver.tick; VStack { A(); B() } }
}
_ = show(Top())
for _ in 0..<3 { driver.tick += 1; spin() }
print(made.sorted { $0.key < $1.key }.map { "\($0.key)=\($0.value)" }.joined(separator: " "))
  • A.init=4 B.init=4 OO.init=1 Plain.init=1
  • A.init=4 B.init=4 OO.init=4 Plain.init=4
  • A.init=1 B.init=1 OO.init=1 Plain.init=1

Mental model. The struct is a sticky note you rewrite every frame; the state is the fridge it is stuck to. Rewriting the note does not buy new food.

The view struct is rebuilt every time a parent’s body runs, so its init runs again and again. The check counts it: after the first render and three parent updates, A.init and B.init are 4 each, but the model object behind @State private var model = Plain() was created once, and so was the one behind @StateObject. In the iOS 27.2 runtime tested here the initial value of @State is evaluated lazily (an autoclosure), the first time SwiftUI needs the storage, and thrown away on later rebuilds, exactly as @StateObject has always done. I did not test older runtimes: on releases before that change, the expression after = could run on every struct init and only the first result was kept, so the safe rule is to keep initialisers cheap and side-effect free anyway.

What you must not do is feed a changing parameter into the initial value and expect it to follow: @State var text: String initialised from an init(text:) parameter keeps the first value forever. If the child should follow the parent, take a plain property or a binding; if the child owns an editable copy, reset it deliberately with .id(value) or onChange. Do work that depends on the view appearing in .task, not in init or the property initialiser, and remember that a model created with @State in a view that is re-created (a different identity) is created again.

The struct is rebuilt on each update; the state’s initial value is created once.

Common traps

  • Doing real work in a view’s init. It runs on every parent update, whether or not the state is kept.
  • Initialising @State from a parameter and expecting it to follow. Only the first value is used.

They might follow up with

  • Does @State with an @Observable class behave the same? Yes: it holds the instance for the lifetime of the view’s identity.
  • How do you pass an initial value in on purpose? State(initialValue:) in init, knowing it applies once.

Related questions

  • 8.3.1 What do @State, @Binding and @Bindable do, and who owns the data?
  • 8.1.2 What is structural versus explicit identity in SwiftUI, and what happens to state when identity changes?
  • 8.4.1 When do onAppear, task and onChange run, and which one do you use for async work?
  • 8.10.2 What is a Binding really, and how do derived, constant and transforming bindings behave?

What is a Binding really, and how do derived, constant and… →