Home What's The Difference Between SwiftUI's @StateObject and @ObservedObject
Post
Cancel

What's The Difference Between SwiftUI's @StateObject and @ObservedObject

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"
}
This post is licensed under CC BY 4.0 by the author.