State makes UI interactive. @State holds a view’s own mutable state and triggers re-renders; @Binding shares that state with child views. Master these and the "value down, actions up" pattern.
Because a view struct is recreated on every change, a plain property cannot hold changing state. @State gives a view a piece of mutable state that SwiftUI stores and watches: reading it subscribes the view, and changing it triggers a re-render. Use @State for simple UI state a single view owns, like a toggle or a text field’s contents.
struct CounterView: View {
@State private var count = 0 // SwiftUI-managed, survives re-renders
var body: some View {
Button("Clicked \(count) times") {
count += 1 // change state -> automatic re-render
}
}
}
// a plain 'var count = 0' would reset on every re-render. @State persists it
// and re-renders the view when it changes. Keep @State private + view-owned.A child view often needs to read and write a parent’s state — a toggle that flips a value the parent owns. @Binding is a two-way reference to state owned elsewhere: the parent passes its state with a dollar-sign prefix, and the child reads and mutates it directly. This keeps a single source of truth while letting child views be interactive.
// child owns NO state — it reads + writes a binding to the parent's state
struct ToggleRow: View {
let label: String
@Binding var isOn: Bool
var body: some View {
Toggle(label, isOn: $isOn) // $isOn passes a binding down
}
}
struct SettingsView: View {
@State private var notifications = true // the source of truth
var body: some View {
ToggleRow(label: "Notifications", isOn: $notifications) // pass a binding
}
}
// '$' turns @State into a @Binding. One source of truth, editable by children.The design principle for reusable SwiftUI views is to keep them stateless where possible — pass the value in and report events out — hoisting the actual state to a parent or a model. A stateless view is easy to preview and test and can be driven by anything. "Value down, actions up" is the same guidance as Compose and React.
// STATELESS: takes a value + a closure -> reusable, previewable, testable
struct CounterDisplay: View {
let count: Int
let onIncrement: () -> Void
var body: some View {
Button("Count: \(count)", action: onIncrement)
}
}
// STATEFUL owner holds the state and passes value down / handles the action
struct CounterScreen: View {
@State private var count = 0
var body: some View {
CounterDisplay(count: count, onIncrement: { count += 1 })
}
}Not all state is @State: compute values from state directly in body (no extra property needed), persist small values across launches with @AppStorage (backed by UserDefaults), and validate inputs inline. Choosing the lightest tool — a computed value, @State, @AppStorage — keeps views simple; heavier app state belongs in an observable model (last lesson).
struct SignUpView: View {
@State private var email = ""
@AppStorage("hasOnboarded") private var hasOnboarded = false // persisted
// derived value — no separate state needed
private var isValid: Bool { email.contains("@") && email.contains(".") }
var body: some View {
VStack {
TextField("Email", text: $email)
Button("Sign up") { hasOnboarded = true }
.disabled(!isValid) // uses the derived value
}
}
}
// @AppStorage persists small values (UserDefaults). For real app data, use an
// observable model — see the Architecture course.