Introduction

SwiftUI uses property wrappers to manage state and data flow in your app. Understanding when and how to use each wrapper is crucial for building robust SwiftUI applications.

@State - Local State Management

@State is used for simple value types that are owned and managed by the view itself.

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

When to use @State

  • For simple, local state
  • Value types (Int, String, Bool, etc.)
  • State that belongs to this view only
  • Always mark as private

@Binding - Two-Way Data Flow

@Binding creates a two-way connection to a value owned by another view.

struct ToggleView: View {
    @Binding var isOn: Bool
    
    var body: some View {
        Toggle("Feature Enabled", isOn: $isOn)
    }
}

// Parent view
struct ParentView: View {
    @State private var featureEnabled = false
    
    var body: some View {
        VStack {
            ToggleView(isOn: $featureEnabled)
            Text(featureEnabled ? "On" : "Off")
        }
    }
}

@ObservedObject - External State

@ObservedObject is used for reference types that conform to ObservableObject.

class UserSettings: ObservableObject {
    @Published var username = ""
    @Published var isDarkMode = false
}

struct SettingsView: View {
    @ObservedObject var settings: UserSettings
    
    var body: some View {
        Form {
            TextField("Username", text: $settings.username)
            Toggle("Dark Mode", isOn: $settings.isDarkMode)
        }
    }
}

@StateObject - View-Owned Object

@StateObject is like @ObservedObject but the view owns the object's lifecycle.

class DataLoader: ObservableObject {
    @Published var data: [Item] = []
    
    init() {
        loadData()
    }
    
    func loadData() {
        // Load data...
    }
}

struct ContentView: View {
    @StateObject private var loader = DataLoader()
    
    var body: some View {
        List(loader.data) { item in
            Text(item.name)
        }
    }
}

@StateObject vs @ObservedObject

Feature @StateObject @ObservedObject
Ownership View owns it Parent owns it
Lifecycle Lives with view Exists outside view
Use case Create instance Receive from parent

@EnvironmentObject - Shared Data

@EnvironmentObject provides shared data throughout the view hierarchy.

class AppState: ObservableObject {
    @Published var currentUser: User?
    @Published var isLoggedIn = false
}

struct RootView: View {
    @StateObject private var appState = AppState()
    
    var body: some View {
        ContentView()
            .environmentObject(appState)
    }
}

struct ContentView: View {
    @EnvironmentObject var appState: AppState
    
    var body: some View {
        if appState.isLoggedIn {
            HomeView()
        } else {
            LoginView()
        }
    }
}

@Environment - System Settings

@Environment accesses system-wide settings and values.

struct ThemedView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack {
            Text(colorScheme == .dark ? "Dark" : "Light")
            Button("Close") {
                dismiss()
            }
        }
    }
}

@AppStorage - UserDefaults Integration

@AppStorage provides a convenient wrapper around UserDefaults.

struct SettingsView: View {
    @AppStorage("hasSeenOnboarding") private var hasSeenOnboarding = false
    @AppStorage("notificationsEnabled") private var notificationsEnabled = true
    
    var body: some View {
        Form {
            Toggle("Notifications", isOn: $notificationsEnabled)
            Button("Show Onboarding") {
                hasSeenOnboarding = false
            }
        }
    }
}

@SceneStorage - State Restoration

@SceneStorage persists state across app launches for scene-based restoration.

struct EditorView: View {
    @SceneStorage("editorText") private var text = ""
    @SceneStorage("selectedTab") private var selectedTab = 0
    
    var body: some View {
        TabView(selection: $selectedTab) {
            TextEditor(text: $text)
                .tabItem { Label("Edit", systemImage: "pencil") }
                .tag(0)
            
            PreviewView(text: text)
                .tabItem { Label("Preview", systemImage: "eye") }
                .tag(1)
        }
    }
}

Best Practices

1. Choose the Right Wrapper

// ❌ Wrong: Using @State for reference types
@State var settings = UserSettings()

// ✅ Right: Use @StateObject
@StateObject var settings = UserSettings()

2. Mark @State as Private

// ❌ Wrong: Public state
@State var count = 0

// ✅ Right: Private state
@State private var count = 0

3. Use $ for Bindings

struct ContentView: View {
    @State private var text = ""
    
    var body: some View {
        // $ creates a Binding from State
        TextField("Enter text", text: $text)
    }
}

Summary

Here's a quick reference guide:

  • @State - Simple local value types
  • @Binding - Pass state to child views
  • @StateObject - View owns the object
  • @ObservedObject - Parent owns the object
  • @EnvironmentObject - App-wide shared state
  • @Environment - System values
  • @AppStorage - UserDefaults wrapper
  • @SceneStorage - Scene state restoration

Conclusion

Understanding SwiftUI property wrappers is essential for effective state management. Each wrapper serves a specific purpose, and using them correctly will make your code cleaner and more maintainable.

Further Reading