iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

@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…