KEY POINT:
@StateObject
: Perfect for creating an ObservableObject inside a main view, while@ObservedObject
is perfect for injecting an ObservableObject into a child view. Try to stay away from using@ObservedObject
to instantiate an ObservableObject because SwiftUI will re-instantiate it every single time the view redraws, which happens quite often with state changes in swiftui
Introduction
When building user interfaces with SwiftUI, it’s essential to manage your view’s state effectively, especially since every data refresh is reactive and view rerenders are side effect of these state changes. Thankfully SwiftUI framework provides two property wrappers, @StateObject
and @ObservedObject
, that allows us to listen for state changes in an ObservableObject
. While both wrappers look similar, they have a fundamental difference that’s important to understand when building apps in SwiftUI.
What’s an ObservableObject?
Ok first off, what’s an ObservableObject
, I would say it sounds pretty descriptive, but for more clarity let’s dig in. An ObservableObject
is simply a protocol that represents an object that contains a publisher or publishers that emits a signal before the object undergoes changes. It provides a way for us to inform SwiftUI to redraw a view when this signal is emitted. This is one of the powerhouse of iOS reactive programming and modern combine framework. With this protocol, we can conform our viewModels to it and annotate properties inside of our viewModels with the @Published property wrapper, and we can be rest assured that whenever that property changes, our viewModel will send a signal to it’s parent view to redraw itself.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import SwiftUI
import Combine
class ProfileViewModel: ObservableObject {
@Published var user: User?
private var cancellable: AnyCancellable?
init(){
fetchUser()
}
func fetchUser() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/users/1") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: User.self, decoder: JSONDecoder())
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.assign(to: \.user, on: self)
}
}
struct ProfileView: View {
/// perfect use of @StateObject since we are using it to create an ObservableObject in a parent view
@StateObject private var viewModel = ProfileViewModel()
var body: some View {
VStack {
if let user = viewModel.user {
ProfileAvatarView(viewModel: viewModel)
Text("Name: \(user.name)")
Text("Email: \(user.email)")
} else {
Text("Loading...")
}
}
}
}
struct ProfileAvatarView: View {
/// perfect use of @ObservedObject since we are using it to pass an ObservableObject from parent to child view
@ObservedObject var viewModel: ProfileViewModel
var body: some View {
if let user = viewModel.user {
Image(user.profileImage)
}
}
}
Ok now that we understand what an ObservableObject
is, and we know we can subscribe to it’s changes either through the @StateObject
or the @ObservedObject
property wrapper, the question now arises, how do we know when to use one over the other, since it sounds like they both do the same thing i.e listen to changes in an ObservableObject
. Well that’s a good question that really matters. Since, if you get this wrong you might find your object getting destroyed randomly, which could cause your app to crash or behavior weirdly. But to answer the question let’s define each property wrapper separately.
@StateObject
@StateObject
is used when we need to create and manage an instance of an ObservableObject within the current view. This means that the current view is the owner of the object’s lifecycle and is responsible for creating and destroying it. We use @StateObject to ensure that the object is created and initialized only once, and that SwiftUI can manage its lifecycle correctly.
@ObservedObject
On the other hand, @ObservedObject
is used when we need to inject an instance of an ObservableObject
into a view as a dependency. This means that the ObservableObject’s lifecycle is managed by its parent view or component, and the current view only observes changes in its state. Instantiating an ObservableObject
with a @ObservedObject
property wrapper is not safe, since the @ObservedObject will reset to default value and not persist through a view’s rerender cycle.
The Differences
One of the cardinal rules in reactive programming is understanding who is the source of truth of a data. The source of truth is the owner and initializer of the data, while the dependent view is typically a child view or component that receives this data through dependency injection. When using either @StateObject
or @ObservedObject
, it’s important to consider whether the view is the source of truth, as this will affect how the state changes propagate through the app.
The rule of thumb in this instance is that if the view subscribing to the ObservableObject
is the source of truth, then the created ObservableObject should be marked with a @StateObject
property wrapper, however if the view is being injected - passed the data by a parent view or component, in that instance we should mark the ObservableObject
property as an @ObservedObject
.
Again, the reason for this is because @StateObject
persists through a view’s redraw cycle, while @ObservedObject
does not. Meaning if you were to mark and initialize a property with @ObservedObject
when the view rerenders itself the current value of the @ObservedObject
will be thrown away and it will reset back to it’s initial value, this is of course not ideal if this not the behavior you’re engineering for, hence the need for us to understand the use case.
Why Even Bother Using @ObservedObject
At first glance, it might seem like using the @StateObject
property wrapper for every instance of ObservableObject
is the way to go. After all, it prevents SwiftUI from discarding the object’s value every time it redraws the view due to a state change. However, this approach can lead to issues with managing the object’s lifecycle. When you use @StateObject
this way, every child view will be asked to retain and manage the object’s lifecycle, even when the parent view already does so. This creates unnecessary redundancy and can make it harder to track down issues with state changes. Therefore, it’s important to carefully consider which property wrapper to use for each instance of ObservableObject
and ensure that your state management system is both efficient and reliable.
TLDR
-
Want to Initialize an
ObservableObject
that belongs to a current view? for instance a loginView’s ViewModel instantiation inside of the loginView should be a@StateObject
. -
Want to Inject - pass an
ObservableObject
from a parent view to a child view? For Instance the loginView’s customTextView component needs access to the loginView’s ViewModel, you should definitely use an@ObservedObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct LoginView: View {
@StateObject private var viewModel = LoginViewModel() // perfect for creating ObservableObject
var body: some View {
CustomTextView(viewModel: viewModel)
}
}
struct CustomTextView: View {
@ObservedObject var viewModel: LoginViewModel // perfect for injecting ObservableObject
var body: some View {
Text(viewModel.welcomeText)
}
}
class LoginViewModel: ObservableObject {
@Published var welcomeText = "Welcome to our app"
}