@State / @Binding
SwiftUI state primitives: @State for local-only, @Binding to expose a slice to a child, @Observable for shared models (Swift 5.9+), @Environment to read framework-injected values, @AppStorage for UserDefaults-backed prefs, @SceneStorage for view-local state that survives backgrounding. Pick the smallest tool that matches the lifecycle.
State, Binding, Observable, Environment patterns
EXAMPLE
import SwiftUI
// 1) @State — owned by the view, dies with the view
struct Counter: View {
@State private var n = 0
var body: some View {
HStack {
Button("-") { n -= 1 }
Text("\(n)").font(.title.weight(.semibold))
Button("+") { n += 1 }
}
}
}
// 2) @Binding — a child mutates a slice owned by the parent
struct AmountStepper: View {
@Binding var amount: Int
var body: some View {
Stepper(value: $amount, in: 0...1000, step: 10) {
Text("Amount: \(amount)")
}
}
}
struct Parent: View {
@State private var qty = 100
var body: some View {
VStack(spacing: 16) {
AmountStepper(amount: $qty) // pass the binding with $
Text("Total: \(qty)")
}
}
}
// 3) @Observable — Swift 5.9+ macro replaces the old @ObservableObject ceremony
@Observable
final class CartModel {
var items: [String] = []
var total: Int = 0
func add(_ id: String, price: Int) {
items.append(id); total += price
}
}
struct CartScreen: View {
@State private var cart = CartModel()
var body: some View {
VStack(alignment: .leading) {
Text("Items: \(cart.items.count)")
Text("Total: \$\(cart.total / 100)")
Button("Add wool jacket") { cart.add("sku-1", price: 19_900) }
}
.padding()
}
}
// 4) @Environment — global-feeling but request-scoped values
struct ThemedRow: View {
@Environment(\.colorScheme) private var scheme
@Environment(\.locale) private var locale
var body: some View {
Text("scheme=\(scheme == .dark ? "dark" : "light") locale=\(locale.identifier)")
}
}
// 5) Inject your own value into the environment
struct AppEnvironment { let apiBase: String }
extension EnvironmentValues {
@Entry var appEnvironment: AppEnvironment = AppEnvironment(apiBase: "https://api.example.com")
}
struct InjectExample: View {
@Environment(\.appEnvironment) private var env
var body: some View { Text(env.apiBase) }
}
@main
struct ShopApp: App {
@State private var cart = CartModel()
var body: some Scene {
WindowGroup {
CartScreen()
.environment(cart)
.environment(\.appEnvironment, AppEnvironment(apiBase: "https://api.shop.example"))
}
}
}
// 6) @AppStorage — Preferences-backed state
struct Prefs: View {
@AppStorage("sortOrder") private var sortOrder = "newest"
var body: some View {
Picker("Sort", selection: $sortOrder) {
Text("Newest").tag("newest")
Text("Price").tag("price")
}
}
}
// 7) @SceneStorage — view-local state that survives backgrounding
struct DraftEditor: View {
@SceneStorage("draftBody") private var body = ""
var body: some View {
TextEditor(text: $body).padding()
}
}
// 8) Decision matrix
// one-screen-only state -> @State
// child mutates a slice -> @Binding
// shared by several views -> @Observable in @State at the root
// global, framework-supplied -> @Environment / inject your own with @Entry
// user preference -> @AppStorage
// per-tab / per-window draft -> @SceneStorage
Why it matters
Reach for @Observable + .environment() over @ObservableObject + @StateObject in any new SwiftUI code. The macro-based observation system only re-renders the views that actually read the changed property, removes the @Published noise, and works with structs you would otherwise wrap in a class.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
@State private var count = 0
var body: some View {
Button("Bump (\(count))") { count += 1 }
}
Try it Yourself »
Discussion
Loading…