@State & @Binding
@State for Local View State
@State is a property wrapper that stores state local to a view. When the value changes, SwiftUI automatically re-renders the view.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
.font(.largeTitle)
Button("Increment") {
count += 1
}
}
}
}
Key characteristics of @State:
- Stored on the view's backing storage, not the struct itself
- Persists across view re-renders (unlike regular properties)
- Marked
privatebecause it is owned exclusively by the declaring view - Works only with value types (String, Int, Bool, structs, arrays)
Why @State Is Needed
Without @State, a property on a SwiftUI view struct would reset every time the view re-renders:
// BUG: This does not work
struct CounterView: View {
var count = 0 // resets on every render!
var body: some View {
Button("Count: \(count)") {
count += 1 // This actually creates a new struct, doesn't mutate
}
}
}
SwiftUI structs are value types and are recreated on every render. @State moves the storage to SwiftUI's managed storage, which persists across renders.
@Binding for Parent-Child Communication
@Binding creates a two-way connection to state owned by another view. The child reads and writes the parent's state.
// Parent owns the state
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "ON" : "OFF")
// Pass binding to child
ToggleRow(isOn: $isOn)
}
}
}
// Child receives a binding
struct ToggleRow: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Enable Feature", isOn: $isOn)
}
}
The $ prefix creates a Binding<Bool> from the @State property. The child can read and write this binding, and the parent's state updates accordingly.
Binding Anatomy
A Binding is a struct that wraps a getter and setter:
// What $isOn creates internally
Binding(
get: { isOn },
set: { isOn = $0 }
)
This two-way connection is why changes in the child immediately reflect in the parent and vice versa.
@Bindable (iOS 17+)
For observable objects, use @Bindable instead of @Binding:
@Observable
class FormViewModel {
var name = ""
var email = ""
}
struct FormView: View {
@Bindable var viewModel: FormViewModel
var body: some View {
Form {
TextField("Name", text: $viewModel.name)
TextField("Email", text: $viewModel.email)
}
}
}
@Bindable creates bindings to properties on an observable object without requiring explicit @Published or @ObservedObject.
Observable Objects
The Need for Reference Type State
@State only works with value types. When you need state that:
- Is shared across multiple views
- Lives longer than a single view
- Contains complex logic (network calls, data processing)
...you need a reference type (class) conforming to Observable.
@Observable (iOS 17+)
The modern approach uses the @Observable macro:
import Observation
@Observable
class UserStore {
var users: [User] = []
var isLoading = false
var errorMessage: String?
func fetchUsers() async {
isLoading = true
defer { isLoading = false }
do {
users = try await apiService.fetchUsers()
} catch {
errorMessage = error.localizedDescription
}
}
}
@Observable automatically tracks which properties are read during body evaluation and triggers updates only when those specific properties change.
Using @Observable in Views
struct UserListView: View {
@State private var store = UserStore()
var body: some View {
List(store.users) { user in
Text(user.name)
}
.overlay {
if store.isLoading {
ProgressView()
}
}
.task { await store.fetchUsers() }
}
}
Note: @State is used with @Observable classes because the store is a reference type. SwiftUI holds a reference to the same instance.
Legacy: @ObservedObject and @Published
Before iOS 17, you used ObservableObject with @Published:
class UserStore: ObservableObject {
@Published var users: [User] = []
@Published var isLoading = false
@Published var errorMessage: String?
func fetchUsers() async {
isLoading = true
defer { isLoading = false }
do {
users = try await apiService.fetchUsers()
} catch {
errorMessage = error.localizedDescription
}
}
}
struct UserListView: View {
@StateObject private var store = UserStore()
// or @ObservedObject if passed from parent
var body: some View {
List(store.users) { user in
Text(user.name)
}
}
}
@StateObject vs @ObservedObject
| Property Wrapper | Ownership | Use When |
|---|---|---|
@StateObject |
Creates and owns the object | The view creates the instance |
@ObservedObject |
Observes but does not own | The object is passed in from a parent |
// @StateObject: view owns the store
struct ContentView: View {
@StateObject private var store = UserStore()
}
// @ObservedObject: parent passes the store
struct UserListView: View {
@ObservedObject var store: UserStore
}
Using @ObservedObject when you should use @StateObject causes the object to be recreated on every render, losing state.
Choosing Between @Observable and ObservableObject
- New projects: Use
@Observable(simpler, no@Publishedboilerplate) - Existing codebases:
ObservableObjectstill works, migrate incrementally - iOS 16 support: Must use
ObservableObject(Observation framework requires iOS 17)
@EnvironmentObject
The Problem of Deep Passing
When many views need the same state, passing it through every level of the view hierarchy becomes tedious:
// Without environment objects - tedious
struct ContentView: View {
@StateObject var store = UserStore()
var body: some View {
NavigationView {
UserListView(store: store) // pass explicitly
}
}
}
struct UserListView: View {
@ObservedObject var store: UserStore
var body: some View {
UserDetailView(store: store) // pass again
}
}
struct UserDetailView: View {
@ObservedObject var store: UserStore
// finally use it
}
Injecting with @EnvironmentObject
@EnvironmentObject makes an object available to all descendant views without explicit passing:
// Injection point (usually at App level)
@main
struct MyApp: App {
@StateObject var store = UserStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}
}
// Any descendant can access it
struct UserDetailView: View {
@EnvironmentObject var store: UserStore
var body: some View {
Text(store.selectedUser?.name ?? "No user")
}
}
Modern Alternative: @Environment with @Observable
With @Observable, you can inject directly into the environment without the Object suffix:
// Injection
@main
struct MyApp: App {
@State var store = UserStore()
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
}
}
}
// Access
struct UserDetailView: View {
@Environment(UserStore.self) var store
var body: some View {
Text(store.selectedUser?.name ?? "No user")
}
}
@Environment for System Values
@Environment also accesses system-provided values like color scheme, locale, and size class:
struct AdaptiveView: View {
@Environment(\.colorScheme) var colorScheme
@Environment(\.horizontalSizeClass) var sizeClass
@Environment(\.locale) var locale
var body: some View {
VStack {
Text("Color scheme: \(colorScheme == .dark ? "Dark" : "Light")")
Text("Size class: \(sizeClass == .compact ? "Compact" : "Regular")")
Text("Locale: \(locale.identifier)")
}
}
}
Environment Precedence
When the same environment value is set at multiple levels, the closest ancestor wins:
NavigationView {
ChildView()
.environment(MyStore()) // This value is used by ChildView
}
.environment(MyStore()) // This is overridden for ChildView's subtree
Best Practices
- Use
@EnvironmentObject/.environment()for truly global state (user auth, theme, settings) - Do not overuse it -- prefer explicit passing for state that is only needed by 1-2 levels
- Always provide a default or handle the missing case, as environment objects are force-unwrapped if missing
- Consider
@Observablewith.environment()as the modern default (iOS 17+)
Quiz
1. Why is @State necessary for view properties?
2. What does the $ prefix do when accessing a @State property?
3. When should you use @StateObject vs @ObservedObject?
4. What is the modern alternative to @EnvironmentObject with @Published?
5. What happens if you use @ObservedObject instead of @StateObject for a locally-created object?
Flashcards
Question
What does @State do?
Click to reveal answer
Answer
It stores view-local state in SwiftUI's managed storage, persisting across view re-renders. Works only with value types.
Question
What is the difference between @StateObject and @ObservedObject?
Click to reveal answer
Answer
@StateObject creates and owns the object; @ObservedObject observes an externally-provided object without ownership.
Question
What does $value create?
Click to reveal answer
Answer
A Binding that provides two-way read/write access to the underlying property value.
Question
How do you inject state for all descendant views?
Click to reveal answer
Answer
Use .environmentObject(obj) or .environment(obj) at an ancestor, then @EnvironmentObject or @Environment(Type.self) in descendants.
Question
What replaced @Published in iOS 17+?
Click to reveal answer
Answer
The @Observable macro, which automatically tracks property access and triggers granular updates.
Revision Notes
Key Takeaways
- 1. @State manages local value-type state that persists across re-renders
- 2. $value creates a Binding for two-way parent-child data flow
- 3. @StateObject owns the object; @ObservedObject does not
- 4. @Observable (iOS 17+) replaces @Published with automatic property tracking
- 5. @EnvironmentObject eliminates tedious prop drilling for shared state
Interview Tips
- • Explain why @State is needed: structs are recreated, @State persists storage
- • Know the difference between @StateObject and @ObservedObject (ownership vs observation)
- • Be ready to describe the $ prefix creating Binding values
- • Understand the migration from ObservableObject/@Published to @Observable
Cheat Sheet
@State: local value-type state, persists across renders.
@Binding: two-way connection to parent's state, use $value to create.
@StateObject: view owns the reference type instance.
@ObservedObject: observes externally-provided reference type.
@Observable (iOS 17+): macro-based, no @Published needed.
@EnvironmentObject / .environment(): inject state for all descendants.
@Environment(SystemValue): access color scheme, locale, size class.