Comparing SwiftUI @StateObject, @ObservedObject and iOS17 @Observed macro

Sasha Myshkina
4 min readSep 13, 2023

--

In the previous article, we looked into the @State property wrapper used with value types. Should we need to create a state property that is object, @StateObject — to the rescue!

Photo by charlesdeluvio on Unsplash

In this piece, you will find out what’s common and different between @StateObject and @ObservedObject. And also, we will look more into the ultimate @Observable macro, provided by iOS 17 and designed to replace them both!

Magic of @StateObject

So, what’s @StateObject? It’s an object with initial value that belongs to the view. It means that it will be kept during the view’s life cycle — both during the initial render and further re-renders. Let’s recreate the example from the previous article using @StateObject instead of @State.

import SwiftUI

// 1 - Here we create ViewModel that
// should conform to the ObservableObject protocol
final class ViewModel: ObservableObject {

// MARK: - Properties

// 2 - Here we declare a property and making it @Published,
// which means we want to be notified when changes occur
@Published var count = 0
}

struct ContentView: View {

// MARK: - Properties

// 3 - Here we declare the @StateObject model property
@StateObject private var model = ViewModel()

// MARK: - Body

var body: some View {
Button(action: {
// 4 - Writing into the model's property
model.count += 1
}, label: {
buttonContent
})
}


private var buttonContent: some View {
HStack {
Image(systemName: "arrow.up")
.imageScale(.large)
.foregroundColor(.accentColor)

// 5 - Reading from the model's property
Text("Count: \(model.count)")
.foregroundStyle(.white)
.padding(.horizontal)
}
.padding()
.background {
Color.black
.cornerRadius(16)
}
}
}

As you can see from the step 1, in order to create @StateObject property model, we need to make the ViewModel class conform to ObservableObject protocol. The official documentation states:

“By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes”.

In the ViewModel class, you can create any @Published property and be sure that view will be updated as soon as the property changes. Think of @Published here as of syntactic sugar because we are able to achieve the same result without using this wrapper like this:

import SwiftUI

final class ViewModel: ObservableObject {

// MARK: - Properties

// We are not marking the property @Published,
// but achieving the same result by utilising the objectWillChange -
// publisher that emits before the object changes.
var count = 0 {
willSet {
objectWillChange.send()
}
}
}

@ObservedObject vs @StateObject

@StateObject, as well as @State, is meant to be used inside the view and shouldn’t be passed from the outside. For the cases when the model needs to be passed in initialiser, we need to use @ObservedObject. The example can be rewritten this way:

import SwiftUI

@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
// Here we pass the model to the view from the outside
ContentView(model: ViewModel())
}
}
}

final class ViewModel: ObservableObject {

// MARK: - Properties

@Published var count = 0
}

struct ContentView: View {

// MARK: - Properties

@ObservedObject var model: ViewModel

// MARK: - Body

var body: some View {
Button(action: {
model.count += 1
}, label: {
buttonContent
})
}
...
}

To wrap it up once again:

Similarities — both @StateObject and @ObservedObject are used to subscribe to the object’s changes on objectWillChange, which is the only requirement of ObservableObject protocol.

Differences — @StateObject is private and should be initialised inside the view. It always has to have some initial value, unlike @ObservedObject . The latter doesn’t need an initial value and can be passed from the outside.

Brand new iOS 17 @Observed macro

Regardless of the model being @StateObject or @ObservedObject, it always conforms to the ObsevableObject protocol. By using this protocol we are explicitly stating that we are waiting for change notifications on the object’s level (a property of the object changes → the whole object changes → the view updates). However, iOS 17 brings the new approach to observing by utilising change notifications on the property level. So the updated example will look as simple as that:

import SwiftUI

// Now we can remove conformance to the ObservableObject protocol
// and use macro @Observable instead
@Observable final class ViewModel {

// MARK: - Properties

var count = 0
}

struct ContentView: View {

// MARK: - Properties

// The model doesn't need to be marked as @ObservedObject,
// but we still can pass it from outside
var model: ViewModel

// MARK: - Body

var body: some View {
Button(action: {
model.count += 1
}, label: {
buttonContent
})
}
...
}

There is much more to it than this simple example. That being the case, the next article will cover the @Observable macro in more detail, and I will happily share some interesting observations.

Stay tuned!

--

--