New SwiftUI State Management

An Updated Comprehensive Guide (2025)

YLabZ
5 min read4 hours ago

Summary

  • @State handles local, mutable state.
  • @Bindable is the new way to automatically create bindings for your data properties, simplifying the parent-to-child state passing process.
  • @Environment continues its role as the conduit for injecting external values throughout your view hierarchy.

Exploring SwiftUI’s New State Management:

SwiftUI has always been a declarative framework that emphasizes a clear separation between your data and its presentation. With the latest updates, Apple has streamlined state management in SwiftUI, making it simpler and more intuitive to work with both value and reference types. This article explores the changes, explains the new property wrappers, and provides practical examples to help you modernize your SwiftUI apps.

A Brief History: Then and Now

The Old Approach

Decision Tree for old SwiftUI State Management

Previously, SwiftUI developers had a handful of property wrappers to manage state:

  • @State: For value types like integers, strings, or even structs.
  • @StateObject: For owning reference types conforming to ObservableObject.
  • @ObservedObject: For observing externally provided reference types.
  • @Binding: For passing state down the view hierarchy.

This model worked well, but it sometimes led to confusion about which wrapper to use for a given scenario, especially when dealing with reference types.

The New Unified Model

Diagram of New SwiftUI State Management

Apple’s latest guidance has unified these concepts by introducing a more streamlined approach:

  • Local Ownership:
    When a view owns its state — whether it’s a value type or a reference type conforming to the new Observable protocol—you now use @State.
  • External Dependencies:
    When state is passed in from a parent or another external source, you use @Observed instead of the old @ObservedObject.

In essence:

  • @StateObject is now @State.
  • @ObservedObject is now @Observed.

This unification reduces boilerplate code and simplifies the developer’s mental model.

NOTE — Choosing between @Binding vs @Bindable

@Binding is ideal for cases where you have a single value (or a small number of values) that you want to pass directly from a parent view to a child view as a two‑way binding. It’s explicit and works well when you only need to expose one piece of state.

@Bindable is designed for your observable models (classes or structs conforming to the new Observable protocol) and automatically creates bindings for multiple properties. This is especially useful when you have a more complex model and you want to reduce boilerplate by not having to create individual @Binding properties for each field.

Deep Dive into the New Property Wrappers

Decision tree

@State: Managing Local State

With the new guidelines, @State becomes your go-to property wrapper for all local state. This includes:

  • Value Types: Such as Int, String, or custom structs.
  • Reference Types: Provided they conform to the new Observable protocol.

Example

Consider a simple view that tracks a counter:

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

In this example, @State handles a value type (Int), ensuring that any updates to count trigger the view to refresh.

@Observed: Receiving External State

For state that is owned by an external source — say, a parent view or a shared model — you now use @Observed. This wrapper is ideal for instances where your view needs to react to changes in a state object that is managed elsewhere.

Example

Imagine a parent view passing down a shared data model:

// A simple model conforming to the new Observable protocol
class SharedModel: Observable {
var title: String = "Hello, SwiftUI!"
}

struct ParentView: View {
@State var model = SharedModel()
var body: some View {
ChildView(model: model)
}
}

struct ChildView: View {
@Observed var model: SharedModel
var body: some View {
Text(model.title)
.padding()
}
}

Here, @Observed is used in ChildView to observe the externally managed SharedModel.

Benefits of the New Approach

Simplified Code

By reducing the number of property wrappers, SwiftUI now offers a more straightforward model:

  • Less Boilerplate: Developers don’t need to decide between @State and @StateObject for local state anymore.
  • Clearer Ownership: It’s immediately clear which state is locally owned and which is being passed in, thanks to the use of @State and @Observed.

Enhanced Performance and Precision

The new observation system brings:

  • Fine-Grained Dependency Tracking: SwiftUI only re-renders the parts of your view that directly depend on the changed state. This results in more efficient UI updates.
  • Seamless Concurrency Integration: The updated APIs work more naturally with Swift’s concurrency features, ensuring that asynchronous state updates are handled smoothly and on the correct thread.

Better Developer Experience

These changes reduce cognitive load, allowing developers to focus on building features rather than managing complex state lifecycles. The unification of state management concepts makes it easier to reason about how data flows through your app.

Transitioning Your Code

If you’re updating an existing codebase, the transition involves:

  1. Replacing @StateObject with @State:
    For any observable object that your view creates, simply swap out @StateObject for @State.
  2. Switching @ObservedObject to @Observed:
    For dependencies provided from outside, change @ObservedObject to @Observed.

Example Refactoring

Before (Old Way):

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

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

After (New Way):

class CounterModel: Observable {
var count = 0
}

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

The refactored version uses @State for local state management, which is now consistent regardless of whether the state is a value type or a reference type.

Conclusion

The new state management model in SwiftUI represents a significant step forward in simplifying and optimizing how we build reactive UIs. By unifying local state management under @State and external state observation under @Observed, Apple has reduced the mental overhead and potential pitfalls associated with managing state in SwiftUI.

These updates not only make your code more concise and readable but also enhance performance by ensuring only the necessary parts of your UI are updated in response to state changes. With improved integration with Swift’s concurrency model and fine-grained dependency tracking, the new approach empowers developers to build smoother, more responsive apps.

For more details, check out the official Apple documentation on SwiftUI state management and start leveraging these best practices in your next SwiftUI project.

~Ash

Reference

--

--

No responses yet