SwiftUI State and Data Flow
Prerequisites: SwiftUI fundamentals, Macros
In SwiftUI, the interface is a function of state: change the data, and the views that depend on it update automatically. The core skill is deciding where each piece of state lives.
@State — state owned by one view
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
.font(.title)
Button("Add one") {
count += 1
}
}
}
}
@State stores the value outside the view struct, so it survives when
SwiftUI re-creates the view. Changing count re-renders the Text.
Mark @State properties private — they belong to this view only.
@Binding — let a child view change a parent's state
struct VolumeSlider: View {
@Binding var volume: Double
var body: some View {
Slider(value: $volume, in: 0...10)
}
}
struct PlayerView: View {
@State private var volume = 5.0
var body: some View {
VStack {
Text("Volume: \(Int(volume))")
VolumeSlider(volume: $volume)
}
}
}
$volume passes a binding — a two-way connection to the parent's
state, not a copy. There's still one source of truth: PlayerView owns
the value; VolumeSlider reads and writes it.
@Observable — shared model objects
For data shared across several views, or with logic attached, use a
class marked with the @Observable macro (iOS 17 / macOS 14 and
later):
import SwiftUI
@Observable
final class CartModel {
var items: [String] = []
var total: Int { items.count * 5 }
func add(_ item: String) {
items.append(item)
}
}
struct ShopView: View {
@State private var cart = CartModel()
var body: some View {
VStack {
Text("Total: $\(cart.total)")
Button("Add coffee") { cart.add("coffee") }
CartBadge(cart: cart)
}
}
}
struct CartBadge: View {
let cart: CartModel
var body: some View {
Text("\(cart.items.count) items")
}
}
- The view that creates the model owns it with
@State. - Views that just use it take it as a plain property — SwiftUI tracks which properties each view reads and updates only those views.
- To pass a model deep into the view tree without threading it through
every initializer, use
.environment(cart)on a parent and@Environment(CartModel.self) private var cartin the child.
(Projects supporting older OS versions use the earlier
ObservableObject protocol with @Published, @StateObject, and
@ObservedObject. You'll see these in existing code.)
Choosing
| Situation | Use |
|---|---|
| Simple value used by one view | @State |
| Child view needs to change a parent's value | @Binding |
| Model with logic, or shared between views | @Observable class |
| Model needed deep in the hierarchy | .environment(...) |
Common mistakes
- Creating a model inside
body(let cart = CartModel()), which recreates it — and loses its data — on every update. Store it in@State. - Duplicating state: copying a value into a child's own
@State, then wondering why the two drift apart. Pass a binding instead.