@Observable vs ObservableObject in SwiftUI
In SwiftIUI, when requiring the reference semantics of a class and wanting to upgrade from Combine’s ObservableObject
and @StateObject
to the new @Observable
it requires a bit more work to get the life-cycle correct. The reason for this is we have to store the object in @State
which is designed for structs and if we just init it inline like a struct as the @State
‘s default value then we’ll have something similar to a memory leak because every time the View is init, another copy of the object will be init. And we know when working in Swift and SwiftUI, unnecessary object heap allocations should be avoided for performance reasons. Since we know that the old @StateObject
is init in onAppear
and deinit in onDisappear
we can just implement that ourselves. See below for a code sample demonstrating the difference between using an ObservableObject
class and an @Observable
class in a SwiftUI view hierarchy. I have commented some points of interest.
import SwiftUI
@Observable class ObservableContent {
var text1 = ""
var text2 = ""
}
class ObservableObjectContent: ObservableObject {
@Published var text1 = ""
@Published var text2 = ""
}
struct ContentView: View {
@State var observableContent: ObservableContent? // must be optional
@StateObject var observableObjectContent = ObservableObjectContent() // this is init is actually delayed until the first appear, via @autoclosure. It is also automatically deinit in onDisappear.
var body: some View {
Group { // allows us to use modifiers on ifs (aka conditional views).
if let observableContent { // required to deal with the optional state.
ObservableContentView(content: observableContent)
}
ObservableObjectContentView(content: observableObjectContent)
}
.onAppear {
if observableContent == nil { // Required because onAppear is also called on re-appear.
observableContent = ObservableContent()
}
}
.onDisappear {
observableContent = nil // To be safe because when state is released is not documented.
}
}
}
struct ObservableContentView: View {
@Bindable var content: ObservableContent
var body: some View {
Form {
TextField("Text1", text: $content.text1)
Text(content.text1)
}
}
}
struct ObservableObjectContentView: View {
@ObservedObject var content: ObservableObjectContent
var body: some View {
Form {
TextField("Text1", text: $content.text1)
Text(content.text1)
}
}
}