SwiftUI State and Data Flow

Make SwiftUI screens interactive with @State, share values with child views through @Binding, keep app data in @Observable models, and read shared objects from the environment.

Lesson 23 of 43Advanced17 min read

In SwiftUI, a view's appearance is a function of its data. You don't change a label's text directly; you change the data, and SwiftUI recalculates body and updates whatever changed on screen. That data is called state, and deciding where each piece of state lives is the most important skill in SwiftUI.

SwiftUI provides a small set of property wrappers for this. @State stores simple values that belong to one view, like whether a sheet is showing. @Binding lets a child view read and write state owned by its parent. The @Observable macro turns a class into a model that views watch automatically, ideal for app data like a shopping basket. And @Environment passes shared values down the view tree without threading them through every initializer.

In this lesson you'll build interactive forms, connect controls to state with bindings, share a model across screens, and learn a simple rule for deciding which wrapper to use.

@State and bindings

Swift
struct SettingsView: View {
    @State private var notifications = true
    @State private var name = ""
    @State private var volume = 0.5

    var body: some View {
        Form {
            TextField("Your name", text: $name)
            Toggle("Notifications", isOn: $notifications)
            Slider(value: $volume)
            Text("Hello, \(name.isEmpty ? "friend" : name)")
        }
    }
}
  • @State gives the view storage that survives when SwiftUI recreates the struct. Mark it private: it belongs to this view.
  • The $ prefix creates a binding, a two-way connection. The TextField reads name and writes back to it as the user types.
  • Controls such as TextField, Toggle, Slider, Stepper, Picker and DatePicker all take bindings.

@Binding for child views

When a child view needs to change state that its parent owns, the child declares @Binding and the parent passes $value:

Swift
struct QuantityPicker: View {
    @Binding var quantity: Int
    var body: some View {
        Stepper("Quantity: \(quantity)", value: $quantity, in: 1...10)
    }
}

// in the parent
@State private var qty = 1
QuantityPicker(quantity: $qty)

There's still only one source of truth: the parent's @State. The binding is just a reference to it. If the child only needs to read the value, use a plain let property instead.

@Observable models

For app data shared by many views, use a class marked with @Observable (iOS 17 and later):

Swift
import Observation

@Observable
final class Basket {
    var items: [String] = []
    var total: Double { Double(items.count) * 2.5 }
    func add(_ item: String) { items.append(item) }
}
  • Views that read items or total update automatically when they change, and views that don't read them aren't touched.
  • The view that creates the model stores it in @State private var basket = Basket().
  • Other views receive it as a normal property, or through the environment with .environment(basket) and @Environment(Basket.self) private var basket.
  • To get bindings to a model's properties, such as for a TextField, declare it @Bindable.

SwiftUI State Syntax

State and data flow

@State private var isOn = false
Toggle("Wi-Fi", isOn: $isOn)
struct Child: View { @Binding var value: Int }
Child(value: $count)
@Observable final class Store { var items: [Item] = [] }
@State private var store = Store()
ContentView().environment(store)
@Environment(Store.self) private var store
@Bindable var store: Store
@Environment(\.dismiss) private var dismiss

SwiftUI State Summary

  • SwiftUI redraws views when their state changes; you change data, not the UI.
  • @State stores private values a view owns; $value creates a binding.
  • @Binding lets a child read and write state owned by its parent.
  • @Observable classes hold shared models; views update when properties they read change.
  • @Environment passes shared models and system values down the view tree.

SwiftUI State Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A tip calculator with @State

Swift
import SwiftUI

struct TipView: View {
    @State private var bill = 40.0
    @State private var tipPercent = 15
    @State private var people = 2

    private var perPerson: Double {
        let total = bill * (1 + Double(tipPercent) / 100)
        return total / Double(people)
    }

    var body: some View {
        Form {
            TextField("Bill", value: $bill, format: .number)
            Picker("Tip", selection: $tipPercent) {
                ForEach([10, 15, 20], id: \.self) { Text("\($0)%") }
            }
            .pickerStyle(.segmented)
            Stepper("People: \(people)", value: $people, in: 1...10)
            Text("Each pays \(perPerson, format: .currency(code: "GBP"))")
                .font(.headline)
        }
    }
}
Output
[Preview: a form with the bill 40, a segmented 10% 15% 20% picker on 15%, "People: 2", and "Each pays £23.00"]
Line-by-line explanation
  1. Lines 4 to 6Three pieces of state owned by this view.
  2. Lines 8 to 11A computed property derives the answer from state, so it never gets out of date. 40 plus 15% is 46, split two ways is 23.
  3. Line 15TextField with a number format edits a Double through a binding.
  4. Lines 16 to 19The segmented picker writes the chosen percentage into tipPercent.
  5. Line 21Changing any control recalculates perPerson and updates the Text.
Example 2

@Binding between parent and child

Swift
import SwiftUI

struct RatingView: View {
    @Binding var rating: Int

    var body: some View {
        HStack {
            ForEach(1...5, id: \.self) { star in
                Image(systemName: star <= rating ? "star.fill" : "star")
                    .foregroundStyle(.yellow)
                    .onTapGesture { rating = star }
            }
        }
    }
}

struct ReviewView: View {
    @State private var rating = 3

    var body: some View {
        VStack {
            RatingView(rating: $rating)
            Text("You chose \(rating) of 5")
        }
    }
}
Output
[Preview: three filled and two empty stars above "You chose 3 of 5"; tapping the fifth star fills all five and the text changes to "You chose 5 of 5"]
Line-by-line explanation
  1. Line 4The child doesn't own the rating; it holds a binding to the parent's value.
  2. Line 11Writing to the binding changes the parent's @State.
  3. Line 18The parent owns the single source of truth.
  4. Lines 22 to 23$rating passes a binding; the parent's Text updates too.
Example 3

A shared @Observable model

Swift
import SwiftUI

@Observable
final class Basket {
    var items: [String] = []
    var count: Int { items.count }
    func add(_ item: String) { items.append(item) }
}

struct ShopView: View {
    @State private var basket = Basket()

    var body: some View {
        VStack(spacing: 12) {
            ProductRow(name: "Mug")
            ProductRow(name: "Lamp")
            Text("Basket: \(basket.count) items")
        }
        .environment(basket)
    }
}

struct ProductRow: View {
    let name: String
    @Environment(Basket.self) private var basket

    var body: some View {
        Button("Add \(name)") { basket.add(name) }
    }
}
Output
[Preview: "Add Mug" and "Add Lamp" buttons above "Basket: 0 items"; each tap increases the count]
Line-by-line explanation
  1. Lines 3 to 8@Observable makes SwiftUI track which properties each view reads.
  2. Line 11The view that creates the model owns it with @State.
  3. Line 19.environment makes the basket available to every view inside.
  4. Line 25Any descendant can read it from the environment by its type.
  5. Line 28Adding an item updates the Text in ShopView, because it reads count.

Common SwiftUI State Mistakes

Using a plain var for changing data

Don't
struct ToggleRow: View {
    var isOn = false                    // can't change, and changes wouldn't redraw
    var body: some View { Toggle("Dark mode", isOn: $isOn) }
}
Do
struct ToggleRow: View {
    @State private var isOn = false
    var body: some View { Toggle("Dark mode", isOn: $isOn) }
}

Copying parent state into the child's @State

Don't
struct Child: View {
    @State var quantity: Int            // a separate copy: the parent never sees changes
}
Do
struct Child: View {
    @Binding var quantity: Int
}

Forgetting to provide an environment object

Don't
ProductRow(name: "Mug")                 // crashes: no Basket in the environment
Do
ProductRow(name: "Mug")
    .environment(basket)

SwiftUI State in Real Projects

State in real SwiftUI apps

Production apps keep their data in a few @Observable models, such as a session, a basket and a settings store, created at the app's root and placed in the environment. Individual screens use @State for local details like whether a sheet is open or what's typed in a search field, and pass bindings to small reusable controls. System values like the colour scheme, dismiss action and locale also come from @Environment.

SwiftUI State FAQ

What is the difference between @State and @Binding in SwiftUI?

@State owns a value and stores it for the view. @Binding doesn't own anything: it's a two-way connection to state owned somewhere else, usually a parent view. The parent declares @State private var qty = 1 and passes $qty to a child that declares @Binding var quantity: Int.

What does the dollar sign mean in SwiftUI?

$ before a state property creates a binding to it, so a control can read and write the value. TextField("Name", text: $name) updates name as the user types. Without the $, you'd pass only the current value, which the control couldn't change.

Why should @State properties be private?

@State is meant for data owned by the view itself. Making it private stops other code setting it from outside, which wouldn't behave as expected because SwiftUI manages the storage. Pass data in through ordinary properties or bindings instead.

What is the difference between @Observable and ObservableObject?

ObservableObject with @Published properties is the older approach, used with @StateObject and @ObservedObject. @Observable, available from iOS 17, needs no @Published, works with plain @State and the environment, and only updates views that actually read the property that changed.

Why is my SwiftUI view not updating when data changes?

The value is probably stored in a plain property instead of state SwiftUI watches. Use @State for view-owned values, @Binding for a parent's values, and an @Observable class for shared models. Also check you're changing the same instance the view is showing, not a copy.

SwiftUI State Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the code to pass a binding to the toggle.

@State private var isOn = false
Toggle("Sound", isOn: isOn)
Multiple choiceeasy

Which property wrapper stores simple state owned by a single view?

Multiple choicemedium

A child view needs to change a value owned by its parent. What should the child declare?

Predict the outputmedium

A view has @State private var count = 0 and a button that runs count += 2 when tapped. What does Text("\(count)") show after three taps?

Multiple choicehard

Where should an @Observable model shared by many screens be created?

SwiftUI State Quiz

5 questions to check what you learned.

  1. What does the $ prefix in $name create?

    • A copy
    • A string
    • An optional
    • A binding, a two-way connection to the value
  2. What happens when a @State value changes?

    • Nothing until the app restarts
    • SwiftUI recomputes the view's body and updates the screen
    • The view is deleted
    • It must be saved manually
  3. What does the @Observable macro do to a class?

    • Makes it a struct
    • Saves it to disk
    • Lets SwiftUI track which properties each view reads and update those views when they change
    • Makes it thread-safe
  4. How does a view read a model placed with .environment(store)?

    • @State var store
    • @Environment(Store.self) private var store
    • @Binding var store
    • It can't
  5. When should a child view use a plain let property instead of @Binding?

    • Never
    • When the value is a class
    • When the value is optional
    • When it only needs to read the value, not change it