When should you use StateObject over ObservedObject and vice versa? That is the question we answer in this tutorial. Both property wrappers play an important role in SwiftUI data flow, but there is a subtle yet elementary difference that sets them apart.

SwiftUI Data Flow

The user interface is a function of the state. That is a foundational principle of SwiftUI. It means that any change in state is automatically reflected in the user interface. SwiftUI makes this possible through a collection of property wrappers, such as @State, @Binding, @StateObject, and @ObservedObject. Each property wrapper plays a clearly defined role in managing and observing the data that flows through your app.

  • @State: A source of truth for data local to a view
  • @Binding: A reference to a source of truth owned by another view
  • @StateObject and @ObservedObject: Designed for observing and owning reference type data

Data Ownership and Lifecycle in SwiftUI

Managing data and understanding how it flows through your app is essential. Because of SwiftUI's reactive nature, understanding the lifecycle of data and how it flows through your app is crucial if your goal is building apps that are performant and predictable.

Understanding data ownership is central to this. When a view owns the data, it is responsible for its creation and destruction. When a view is merely observing the data, though, the data's lifecycle is managed elsewhere. This distinction is important, especially when you are dealing with data that is shared across multiple views. So using the wrong property wrapper can result in unexpected behavior (read: bugs).

StateObject Property Wrapper

What Is StateObject?

The StateObject property wrapper was introduced in 2020 alongside iOS 14, tvOS 14, macOS 11, and watchOS 7. It acts as a source of truth for reference types within a SwiftUI view. Unlike other property wrappers, a StateObject is responsible for both creating and managing the lifecycle of the wrapped object. What does that mean? The object associated with a StateObject persists for the lifetime of the view that owns it.

When to Use StateObject?

You should use the StateObject property wrapper for properties that are (1) initialized once and (2) owned by the view.

Initialized once: When the body of a view is recomputed due to an external data change or a redraw operation, properties wrapped with StateObject are unaffected.

Owned by the view: The data's lifecycle is tied to that of the view. The data is deallocated as soon as the view that owns the data is deallocated. That makes sense. Right?

How to Use StateObject?

Creating and using a StateObject is straightforward. You simply prefix the property declaration with the StateObject property wrapper. The only requirement is that the type of the property conforms to the ObservableObject protocol.

@StateObject var viewModel = NotesViewModel()

The ObservableObject protocol can only be adopted by references types, which means that the StateObject property wrapper only works with reference types, not value types (enums, structs).

Let's take a look at an example. I'm a big fan of the Model-View-ViewModel pattern so let's use that as an example.

import SwiftUI

struct NotesView: View {

    // MARK: - Properties

    @StateObject private var viewModel = NotesViewModel()

    // MARK: - View

    var body: some View {
        List(viewModel.notes, id: \.self) { note in
            Text(note.title)
        }
    }

}

final class NotesViewModel: ObservableObject {

    // MARK: - Properties

    @Published private(set) var notes: [Note] = []

}

In this example, NotesView owns an instance of NotesViewModel via StateObject. The result is that the list of notes is consistent across redraws of the NotesView.

Benefits and Limitations of StateObject

Thanks to the StateObject property wrapper the wrapped object is initialized once and persists across view updates. This isn't true for the State property wrapper. Because the view owns the data, it can guarantee the data is consistent across redraws. The obvious limitation is that the StateObject property wrapper can only be used with reference types because only reference types can conform to the ObservableObject protocol.

ObservedObject Property Wrapper

You now know that the StateObject property wrapper plays an important role in owning and managing data within a SwiftUI view. Not every piece of data needs or should be owned by the view that displays it, though. That is where the ObservedObject property wrapper comes into play. Unlike StateObject, ObservedObject is designed to observe and respond to changes in reference types without taking ownership.

What Is ObservedObject?

ObservedObject is a property wrapper that binds a SwiftUI view to a source of truth that isn't owned by the view. The source of truth needs to conform to the ObservableObject protocol so that means only reference types qualify. As I wrote earlier, an ObservedObject doesn't own or manage the lifecycle of the object it observes. It merely listens to changes in the ObservableObject and triggers updates of the view.

When to Use ObservedObject?

Choose for the ObservedObject property wrapper if the data is external. What do I mean by that? If a view needs to display data that is owned and managed by an external source, such as a parent view or a shared data store, then opt for ObservedObject.

How to Use ObservedObject?

The ObservedObject property wrapper is easy to use and in some ways similar to StateObject. You prefix the property declaration with the ObservedObject property wrapper. Like StateObject, the only requirement is that the type of the property conforms to the ObservableObject protocol.

@ObservedObject var store: NotesStore

Let's take a look at another example. The data the NotesView displays is managed by an instance of the NotesStore class. The NotesStore instance isn't created or managed by the view.

import SwiftUI

struct NotesView: View {

    // MARK: - Properties

    @ObservedObject var store: NotesStore

    // MARK: - View

    var body: some View {
        List(store.notes, id: \.self) { note in
            Text(note.title)
        }
    }

}

final class NotesStore: ObservableObject {

    // MARK: - Properties

    @Published private(set) var notes: [Note] = []

}

We use ObservedObject instead of StateObject because the view doesn't create and manage the NotesStore. The NotesStore shouldn't be deallocated when the view is deallocated.

Benefits and Limitations of ObservedObject

The ObservedObject property wrapper allows the view to update its body if an external data change occurs. It makes it easier to share data among views. Unlike StateObject, ObservedObject doesn't manage the lifecycle of the object it observes so you need to make sure the lifecycle of the object is managed elsewhere in the app.