SwiftUI Bites is a short-form series for developers (including UIKit converts)
explaining SwiftUI concepts quickly, clearly, and with real-world examples.
Today, we’ll take a closer look at state management in SwiftUI using property wrappers.
You’ve likely seen them before — and probably used them — but this article brings everything together in one place.
Quick Help: Choosing the Right SwiftUI Property Wrapper
If you’re already familiar with SwiftUI’s state property wrappers and just need a quick refresher, start with the decision model and matrix below.
For everyone else, we’ll walk through the details and examples right after.

SwiftUI State Property Wrappers — Quick Lookup
| Property Wrapper | Decision Flow Question | Data Type | Ownership | When the View Updates |
|---|---|---|---|---|
@EnvironmentObject | Is this app-wide state? → Yes | Reference (ObservableObject) | App / Scene | Any published change anywhere in the app |
@State | Not app-wide → Value type | Value (Bool, Int, String, struct) | View | Local value mutation |
@StateObject | Object → Created by this view | Reference (ObservableObject) | View | Published changes from the object |
@ObservedObject | Object → Created externally | Reference (ObservableObject) | Parent / external | Published changes from the object |
@Binding | Does a child need to modify it? → Yes | Usually value types | Parent view | Writes propagate back to the owner |
Understanding SwiftUI State Property Wrappers
This article is based on a small demo app that shows each SwiftUI state property wrapper in action.
You can find the full, runnable project on GitHub:
👉 https://github.com/Wooder/SwiftUIBites-StateManagement
Local Value State: @State and @Binding
Let’s start with the simplest form of state in SwiftUI: local value state.
This is the kind of state that:
- belongs to a single view
- represents simple values like
Bool,Int, orString - drives local UI updates
Typical examples include counters, toggles, selections, or temporary UI state.
@State: View-owned value state
@State is used when a view owns a piece of value-type state.
@State private var count: Int = 0
Even though SwiftUI views are value types, @State is stored externally by SwiftUI.
This means the value survives view re-creations and triggers a view update whenever it changes.
Key characteristics of @State:
- the state is owned by the view
- only the owning view should mutate it
- changes automatically re-render the view
If a value is local to a view and does not need to be shared broadly, @State is usually the correct choice.
@Binding: Write access to parent-owned state
Often, a child view needs to modify state that is owned by its parent.
This is where @Binding comes in.
@Binding var count: Int
A binding does not store state itself.
Instead, it provides read/write access to state that is owned elsewhere.
In practice, this means:
- the parent keeps ownership using
@State - the child receives a binding using
@Binding - changes in the child propagate back to the parent automatically
This preserves a clear ownership model while still allowing child views to participate in state changes.
Putting it together
In the demo app, this pattern is used to implement a simple dice roll:
- The parent view owns the value using
@State - A child view updates the value via
@Binding - SwiftUI re-renders the UI whenever the value changes
This keeps responsibilities clear:
- ownership stays with the parent
- interaction logic can live in the child
Demo project reference
You can find a complete working example in the demo project:
DiceView.swift— owns the value using@StateDiceViewActions.swift— modifies it via@Binding
Running the app and interacting with this view makes the data flow immediately visible.
Common pitfall
A frequent mistake is trying to use @State in both the parent and the child.
This creates two independent states that quickly fall out of sync.
If a child needs to modify parent-owned value state, the correct solution is almost always @Binding.
In the next section, we’ll move from value types to reference types and look at how ownership works for ObservableObject using @StateObject and @ObservedObject.
Reference State and Ownership: @StateObject vs @ObservedObject
Not all state in SwiftUI is a simple value.
As soon as you work with reference types — typically view models conforming to ObservableObject — ownership and lifecycle become the deciding factors.
This is where @StateObject and @ObservedObject come into play.
ObservableObject: Reference-based state
A typical view model in SwiftUI looks like this:
final class CounterViewModel: ObservableObject {
@Published private(set) var count: Int = 0
func increment() {
count += 1
}
}
Key points:
- it’s a reference type
- it emits change notifications via
@Published - multiple views can observe the same instance
Unlike value state, reference state has a lifecycle — and SwiftUI needs to know who owns it.
@StateObject: The owning view
Use @StateObject when a view creates and owns an observable object.
@StateObject private var vm = CounterViewModel()
@StateObject guarantees that:
- the object is created only once
- the instance survives view re-renders
- SwiftUI manages its lifecycle correctly
In other words:
- this view is the source of truth
- this view is responsible for creating the object
If a view creates a view model, @StateObject is the correct choice.
@ObservedObject: Observing external state
Child views often need access to a view model that is owned elsewhere.
In that case, use @ObservedObject.
@ObservedObject var vm: CounterViewModel
With @ObservedObject:
- the view does not own the object
- it assumes the object already exists
- it simply reacts to published changes
This avoids accidental re-creation of the object and keeps ownership explicit.
Putting it together
In the demo app, this pattern is used for a simple counter:
CounterViewcreates and owns the view model using@StateObjectCounterActionsreceives the same instance via@ObservedObject- when the counter changes, SwiftUI re-renders both views automatically
This separation keeps responsibilities clear:
- one owner
- many observers
- predictable lifecycle behavior
Demo project reference
You can explore this pattern in the demo project:
CounterView.swift— creates the view model using@StateObjectCounterActions.swift— observes it via@ObservedObject
Running the app and tapping the buttons makes the ownership model visible in action.
Common pitfall
A very common mistake is using @ObservedObject to create a view model:
@ObservedObject var vm = CounterViewModel() // ❌
This causes the object to be recreated whenever the view is re-rendered, leading to lost state and subtle bugs.
If a view creates the object, it must use @StateObject.
In the next section, we’ll look at app-wide state and how @EnvironmentObject allows you to share data across multiple, unrelated views without manual prop drilling.
App-Wide State: @EnvironmentObject
So far, all examples used explicit data flow:
- state is created in a parent
- passed down to children via initializers
This works well — until state needs to be shared across many unrelated views.
That’s where @EnvironmentObject comes in.
What @EnvironmentObject is for
@EnvironmentObject is used for app-wide or feature-wide state that:
- is needed by many views
- is not tied to a single view hierarchy
- should not be passed through multiple initializers
Typical examples include:
- user sessions
- authentication state
- app settings
- feature flags
Injecting the environment object
An environment object is created at a high level in the app and injected into the view hierarchy.
@main
struct SwiftUIBites_StateManagementApp: App {
@StateObject private var session = UserSession()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(session)
}
}
}
Here:
- the app creates and owns the object using
@StateObject - the object is injected once at the root
- all child views can access it without explicit wiring
Reading from the environment
Any view in the hierarchy can read the injected object using @EnvironmentObject.
@EnvironmentObject private var session: UserSession
When the environment object changes:
- SwiftUI automatically re-renders all dependent views
- no manual updates are required
This makes @EnvironmentObject feel almost “global” — but it’s still scoped to the view hierarchy.
Writing to the environment
Environment objects are not read-only.
In the demo app, the profile view updates the session state:
TextField("Username", text: $session.username)
When the user changes the username:
- the environment object updates
- all other views observing it are re-rendered automatically
This makes @EnvironmentObject ideal for shared, mutable app state.
Demo project reference
You can see this pattern in action in the demo project:
SwiftUIBites_StateManagementApp.swift— injects the environment objectProfileView.swift— modifies the shared stateCounterView.swiftandDiceView.swift— read from it
Running the app and switching between views makes the shared nature of the state very clear.
Common pitfalls
Forgetting to inject the environment object
If a view uses @EnvironmentObject but the object is not injected, the app will crash at runtime.
Always ensure the object is provided at the root of the hierarchy.
Overusing @EnvironmentObject
Not every shared value belongs in the environment.
If state is:
- only used by a parent and its direct children
- feature-local
- short-lived
Passing it explicitly or using @State / @Binding is usually the better choice.
Mental model
Think of @EnvironmentObject as:
- shared ownership
- implicit access
- explicit responsibility
Use it sparingly, but confidently, when state truly belongs to the app as a whole.
Wrapping up
At this point, you’ve seen all core SwiftUI state property wrappers in action:
- local value state with
@State - shared value access with
@Binding - reference state ownership with
@StateObject - reference state observation with
@ObservedObject - app-wide shared state with
@EnvironmentObject
Together, these tools form a complete and predictable state management model for SwiftUI apps.
Summary
SwiftUI’s state property wrappers are less about syntax and more about ownership, scope, and data flow.
Once you understand who owns a piece of state and who is allowed to change it, the choice of property wrapper becomes straightforward.
As a rule of thumb:
- Use
@Statefor local, view-owned value state - Use
@Bindingto give a child write access to parent-owned value state - Use
@StateObjectwhen a view creates and owns an observable object - Use
@ObservedObjectwhen a view observes an object owned elsewhere - Use
@EnvironmentObjectfor shared, app-wide state
The decision model and demo project show that SwiftUI state management is not magic — it’s a small set of consistent rules applied in the right places.
If you ever feel unsure which wrapper to use, start by asking:
- Is this state local or shared?
- Is it a value or a reference?
- Who owns it?
Answering those questions will almost always lead you to the correct solution.



