Comparing SwiftUI @StateObject, @ObservedObject and iOS17 @Observed macro
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!
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!