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.