SwiftUI’s property wrappers



This content originally appeared on DEV Community and was authored by Harsh Prajapat

SwiftUI’s property wrappers are like magical tools that help you manage state, data flow, and environment context in a declarative way. Let’s break down the most important ones so you can see how they fit together. 🧠✨

🧵 Core Property Wrappers in SwiftUI

Wrapper Purpose Ownership Typical Use
@State Local value-type state ✅ Owns data Simple UI state (e.g. toggles, counters)
@Binding Two-way connection to another value ❌ Refers to external data Pass state between parent and child views
@StateObject Owns a reference-type observable object ✅ Owns data Create and manage ObservableObject instances
@ObservedObject Observes external ObservableObject ❌ Refers to external data Watch changes in shared objects
@EnvironmentObject Access shared object from environment ❌ Refers to external data Share data across many views
@Environment Read system/environment values ❌ Refers to external data Access traits like color scheme, locale
@AppStorage Read/write to UserDefaults ✅ Owns data Persist user settings (e.g. theme, login)
@Published Notify views of changes inside ObservableObject ✅ Owns data Mark properties that trigger view updates
@Observable New macro replacing ObservableObject + @Published ✅ Owns data Cleaner, more efficient state tracking
@Bindable Enables bindings to @Observable properties ❌ Refers to external data Use $property syntax with @Observable

🧪 Example: Using @State and @Binding

struct ParentView: View {
    @State private var isOn = false

    var body: some View {
        ToggleView(isOn: $isOn)
    }
}

struct ToggleView: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Enable Feature", isOn: $isOn)
    }
}

🧬 Example: Using @StateObject and @ObservedObject

class CounterModel: ObservableObject {
    @Published var count = 0
}

struct CounterView: View {
    @StateObject private var model = CounterModel()

    var body: some View {
        VStack {
            Text("Count: \(model.count)")
            Button("Increment") {
                model.count += 1
            }
        }
    }
}

If you pass model to another view, use @ObservedObject there.

🌍 Example: Using @EnvironmentObject

class Settings: ObservableObject {
    @Published var isDarkMode = false
}

struct RootView: View {
    @StateObject private var settings = Settings()

    var body: some View {
        ContentView()
            .environmentObject(settings)
    }
}

struct ContentView: View {
    @EnvironmentObject var settings: Settings

    var body: some View {
        Toggle("Dark Mode", isOn: $settings.isDarkMode)
    }
}

Want to dive deeper into @Observable, @Bindable, or how these wrappers interact with SwiftUI’s rendering engine? I can walk you through advanced patterns or help refactor your code. 🧵💬


This content originally appeared on DEV Community and was authored by Harsh Prajapat