SwiftUI Bites #01 – State Management & Property Wrappers – when to use what

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 WrapperDecision Flow QuestionData TypeOwnershipWhen the View Updates
@EnvironmentObjectIs this app-wide state? → YesReference (ObservableObject)App / SceneAny published change anywhere in the app
@StateNot app-wide → Value typeValue (Bool, Int, String, struct)ViewLocal value mutation
@StateObjectObject → Created by this viewReference (ObservableObject)ViewPublished changes from the object
@ObservedObjectObject → Created externallyReference (ObservableObject)Parent / externalPublished changes from the object
@BindingDoes a child need to modify it? → YesUsually value typesParent viewWrites 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, or String
  • 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 @State
  • DiceViewActions.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 ObservableObjectownership 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:

  • CounterView creates and owns the view model using @StateObject
  • CounterActions receives 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 @StateObject
  • CounterActions.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 object
  • ProfileView.swift — modifies the shared state
  • CounterView.swift and DiceView.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 @State for local, view-owned value state
  • Use @Binding to give a child write access to parent-owned value state
  • Use @StateObject when a view creates and owns an observable object
  • Use @ObservedObject when a view observes an object owned elsewhere
  • Use @EnvironmentObject for 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.

Leave a Reply

Your email address will not be published. Required fields are marked *