State and data flow · core

State and bindings

How a struct that gets thrown away constantly can nevertheless remember things — and how a child view changes its parent's data.

12 min read15 min practice0/2 exercises4 recall cards

By the end you will be able to

  • Explain where @State actually stores its value and why views can be structs
  • Pass write access to a child with @Binding and the $ prefix
  • Choose between @State, @Binding and a plain let
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

A SwiftUI view is a struct that is destroyed and recreated constantly. So where does @State private var count = 0 keep its value between recreations?

@State — this view owns this value

struct Counter: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Add one") {
                count += 1
            }
        }
    }
}

Three things happen because of that one attribute:

  1. The value lives in SwiftUI's storage, not in the struct, so it survives re-creation.
  2. Writing to it marks the view as needing an update, and body runs again.
  3. You can mutate it from inside body's closures even though self is a let struct.
Static preview: the button records its action but does not run it here.
Run to see the preview.

@Binding — a child writes to the parent's value

A child view that only reads takes a plain let. A child that must write takes a @Binding:

struct Stepper2: View {
    @Binding var value: Int          // read-write reference to someone else's state

    var body: some View {
        HStack {
            Button("−") { value -= 1 }
            Text("\(value)")
            Button("+") { value += 1 }
        }
    }
}

struct Parent: View {
    @State private var quantity = 1

    var body: some View {
        Stepper2(value: $quantity)   // $ makes a Binding from the State
    }
}

The $ prefix gives you the property wrapper's projected value. For @State, that is a Binding — a pair of get and set closures pointing back at the original storage. The child does not own the value; it holds a two-way connection to it.

SwiftUI preview
Run to see the preview.

One QuantityRow type serves both rows, each bound to a different piece of the parent's state. That is the whole pattern.

Bindings everywhere

Every SwiftUI control that edits a value takes a binding:

TextField("Name", text: $name)
Toggle("Notifications", isOn: $enabled)
Slider(value: $volume, in: 0...1)
Picker("Theme", selection: $theme) { … }

You can also derive a binding from a property of a bound value — $user.name works if $user is a Binding<User> — which is why SwiftUI forms can edit nested model structs directly.

The decision table

SituationUse
This view owns the value and no one else touches it@State private var
A child needs to change a value the parent owns@Binding var, passed as $value
A child only needs to display itplain let
Several unrelated views share itan observable model — next lesson

Reach for the least powerful option that works. A plain let is easier to reason about than a binding, and a binding is easier than shared model state.

The hidden storage property

Writing @State private var text = "" generates two things: a hidden stored property _text of type State<String>, and a computed text that reads and writes through it. $text is _text.projectedValue.

That is why you occasionally see this in real code:

init(initial: String) {
    _text = State(initialValue: initial)
}

It seeds the state from an initializer. And it comes with the warning that makes it worth knowing about: that seeding happens only on first creation. If the parent later passes a different initial, this view ignores it, because SwiftUI keeps the existing storage for that position in the hierarchy.

Feeding changing external data into @State is one of the most common SwiftUI bugs. If the value comes from outside, it belongs in a let or a @Binding.

Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

Two independent counters, each with its own @State. What is printed when this view is built?

struct Counter2: View {
    let name: String
    @State private var count = 0

    var body: some View {
        Text("\(name): \(count)")
    }
}

func describe() {
    print("Each Counter2 gets its own storage slot.")
    print("Sharing a struct definition does not share state.")
}

describe()
Check yourself

A child view needs to display a value but never change it. What should its property be?

Wire up a binding

Build the view

ToggleRow should be able to change the parent's value. Fix its property and the way it is passed in.

Solution unlocks after 3 attempts

Own the state

Build the view

Give SearchBar its own private state for the query and show the current value beneath the field.

Solution unlocks after 3 attempts
Explain itSaved on this device. Never graded.

Explain why a SwiftUI view can be a let struct and still contain a counter that changes. Your explanation should mention where the value actually lives.

Checkpoint

You can now:

  • Explain where @State stores its value and why views can be structs
  • Pass write access downward with @Binding and $
  • Apply the least-powerful-tool rule to let, @Binding and @State
  • Recognise the bug of feeding changing external data into @State

Next up: @Observable — the modern way to share model state across many views.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.