Give your simulator superpowers

Give your Xcode
Simulator extra features

@StateObject vs. @ObservedObject: The differences explained

The @StateObject and @ObservedObject property wrappers tell a SwiftUI view to update in response to changes from an observed object. Both wrappers look similar but have an essential distinction to be aware of when building apps in SwiftUI.

At first, you might wonder why you wouldn’t just always use @ObservedObject. I thought the same for a while since both looked so similar to me. However, I only started to realize how badly I needed @StateObject in some cases once I knew about its purpose in SwiftUI.

What is an @ObservedObject?

Before diving into the differences between @StateObject and @ObservedObject, it’s good to understand what an @ObservedObject is. Both property wrappers require your object to conform to the ObservableObject protocol. This protocol stands for an object with a publisher that emits before the object has changed and allows you to tell SwiftUI to trigger a view redraw.

Types conforming to the ObservableObject can be combined with the @ObservedObject property wrapper to connect the SwiftUI view and make it redraw after a detected change inside the observed object.

A classic example would be a counter view:

final class CounterViewModel: ObservableObject {
    @Published var count = 0

    func incrementCounter() {
        count += 1
    }
}

struct CounterView: View {
    @ObservedObject var viewModel = CounterViewModel()

    var body: some View {
        VStack {
            Text("Count is: \(viewModel.count)")
            Button("Increment Counter") {
                viewModel.incrementCounter()
            }
        }
    }
}

We can define our view model as @ObservedObject since the CounterViewModel conforms to the ObservableObject protocol. The count property inside the view model increments once the button fires, and the @Published property makes sure all observers receive a changed signal.

To better understand how a change delegates through observers, we could modify the above code to no longer use the @Published property wrapper. Instead, we’ll make use of the observable object publisher and send a change signal manually:

final class CounterViewModel: ObservableObject {
    private(set) var count = 0

    func incrementCounter() {
        count += 1
        objectWillChange.send()
    }
}

You’ll see the view still updates once the increment method executes. In most cases, we don’t need to call into the objectWillChange.send() method manually. However, it might be a better solution if you’re updating multiple published properties at once to mark those properties as regular arguments and use the manual signal instead.

The above examples demonstrate how a SwiftUI view redraws based on detected changes inside the observed object. Let’s dive into the @StateObject property wrapper to see how it differs.

What is a @StateObject?

The @StateObject property wrapper works similarly to the @ObservedObject. We could change our previous code example to use the @StateObject property wrapper instead, and nothing seems to be changed:

struct CounterView: View {
    /// Using @StateObject instead of @ObservedObject
    @StateObject var viewModel = CounterViewModel()

    var body: some View {
        VStack {
            Text("Count is: \(viewModel.count)")
            Button("Increment Counter") {
                viewModel.incrementCounter()
            }
        }
    }
}

The fact that nothing changes is precisely why I often decided to use @ObservedObject. However, there’s a significant difference in making it clear when to use @StateObject instead of @ObservedObject.

Observed objects marked with the @StateObject property wrapper don’t get destroyed and re-instantiated at times their containing view struct redraws. Understanding this difference is essential in cases another view contains your view.

To demonstrate how this works, we can wrap our earlier used counter view inside another view:

struct RandomNumberView: View {
    @State var randomNumber = 0

    var body: some View {
        VStack {
            Text("Random number is: \(randomNumber)")
            Button("Randomize number") {
                randomNumber = (0..<1000).randomElement()!
            }
        }.padding(.bottom)
        
        CounterView()
    }
}

The random number view allows you to generate a random number by pressing the randomize button. The randomNumber property marked by the @State property wrapper will redraw the view causing our CounterView to be regenerated.

You’ll see the counter resetting if we use @ObservedObject inside our CounterView once a new random number generates:

The counter resets since we’re using @ObservedObject instead of @StateObject

The fix is as simple as changing the CounterView view model property to be a @StateObject instead of an @ObservedObject. As described above, the state object makes sure the view model retains between view redraws and ensures our counter values remain the same.

So, when should I use a @StateObject?

Right, we now know how both work, but it might be good to understand better when to use the @StateObject over the @ObservedObject property wrapper.

It’s unsafe to create an @ObservedObject inside a view since SwiftUI might create or recreate a view at any time. Unless you inject the @ObservedObject as a dependency, you want to use the @StateObject wrapper to ensure consistent results after a view redraw.

Should I use @StateObject for all views using the same instance?

Siblings observing the same @StateObject instance down the line don’t require marking the object with this property wrapper. It’s important not to do this since you’ll ask to retain and manage the object’s lifecycle in two places. As described in the previous section: if you inject the observed object, you should use @ObservedObject.

Conclusion

@StateObject and @ObservedObject have similar characteristics but differ in how SwiftUI manages their lifecycle. Use the state object property wrapper to ensure consistent results when the current view creates the observed object. Whenever you inject an observed object as a dependency, you can use the @ObservedObject.

If you like to improve your SwiftUI knowledge, even more, check out the SwiftUI category page. Feel free to contact me or tweet me on Twitter if you have any additional tips or feedback.

Thanks!

 

Featured SwiftLee Jobs

Loading RSS Feed

Browse more Swift related Jobs, or add your own on SwiftLee Jobs