SwiftUI, Easily to Understand: ObservedObject vs StateObject with Code Example
In some simple cases, the behavior between @StateObject
and @ObservedObject
might not seem different, especially if the app doesn't trigger SwiftUI to re-render a Screen by removing it from the hierarchy.
To demonstrate the real difference between @StateObject
and @ObservedObject
, we need to introduce a scenario where Screen1
is removed from the view hierarchy and then re-created. This is when the issue with @ObservedObject
will become apparent, as it will cause the viewModel
to be reinitialized, unlike @StateObject
.
Example: Triggering the Difference Between @StateObject
and @ObservedObject
The key difference is seen when the view is removed from the hierarchy and reinserted (such as by navigating away from the view and then back to it). This behavior forces SwiftUI to recreate the view, which exposes the problem with @ObservedObject
to initialized it inside the View.
Inside the Code Example:
- I’ve added a toggle button to show or hide
Screen1
, which will simulate removing the view from the hierarchy. - The wrong behavior occurs when using
@ObservedObject
becauseContentViewModel
is reinitialized each timeScreen1
is re-added, which leads to a reset of thecount
.
Here’s the complete code showing the difference between @StateObject
and @ObservedObject
:
With @ObservedObject
(Wrong Implementation):
import SwiftUI
class ContentViewModel: ObservableObject {
@Published var count = 0
func increment() {
count += 1
}
}
struct Screen1: View {
@ObservedObject var viewModel = ContentViewModel() // Wrong: Reinitializes every time
var body: some View {
VStack {
Text("Screen 1 - Count: \(viewModel.count)")
.padding()
Button("Increment") {
viewModel.increment()
}
.padding()
NavigationLink(destination: Screen2(viewModel: viewModel)) {
Text("Go to Screen 2")
}
.padding()
}
}
}
struct Screen2: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Screen 2 - Count: \(viewModel.count)")
.padding()
Button("Increment") {
viewModel.increment()
}
.padding()
}
}
}
struct ContentView: View {
@State private var showScreen1 = true
var body: some View {
VStack {
if showScreen1 {
Screen1() // Recreates Screen1 each time this is toggled
}
Button("Toggle Screen 1") {
showScreen1.toggle()
}
.padding()
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Steps to Test:
- Run the app.
- On Screen1, press the Increment button a few times to increase the count.
- Press the Toggle Screen 1 button to hide and then show Screen1 again.
- Notice that the counter resets to 0 each time you toggle the view. This happens because
@ObservedObject
reinitializesContentViewModel
each time Screen1 is recreated, causing data loss.
With @StateObject
(Correct Implementation):
import SwiftUI
class ContentViewModel: ObservableObject {
@Published var count = 0
func increment() {
count += 1
}
}
struct Screen1: View {
@StateObject var viewModel = ContentViewModel() // Correct: Retains state across view updates
var body: some View {
VStack {
Text("Screen 1 - Count: \(viewModel.count)")
.padding()
Button("Increment") {
viewModel.increment()
}
.padding()
NavigationLink(destination: Screen2(viewModel: viewModel)) {
Text("Go to Screen 2")
}
.padding()
}
}
}
struct Screen2: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
VStack {
Text("Screen 2 - Count: \(viewModel.count)")
.padding()
Button("Increment") {
viewModel.increment()
}
.padding()
}
}
}
struct ContentView: View {
@State private var showScreen1 = true
var body: some View {
VStack {
if showScreen1 {
Screen1() // Screen1 is only initialized once
}
Button("Toggle Screen 1") {
showScreen1.toggle()
}
.padding()
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Steps to Test:
- Run the app with the corrected code (
@StateObject
inScreen1
). - On Screen1, press the Increment button a few times to increase the count.
- Press the Toggle Screen 1 button to hide and then show Screen1 again.
- Notice that the counter retains its value and does not reset to 0. This is because
@StateObject
ensures that theContentViewModel
is initialized only once, and its state is preserved across view updates.
Explanation of the Difference:
- With
@ObservedObject
: Each timeScreen1
is recreated (toggled in or out of the view hierarchy), theviewModel
is reinitialized, causing the counter to reset to 0. - With
@StateObject
: TheviewModel
is only created once and persists across view updates, so the counter retains its value even when the view is removed and reinserted into the hierarchy.
This test should clearly show you the difference in behavior between @ObservedObject
and @StateObject
when the view is removed and re-added to the view hierarchy.
Summary of Best Practices:
- Use
@StateObject
when the view creates and owns the object and is responsible for maintaining its lifecycle across view updates. - Use
@ObservedObject
when the view is observing an object passed from elsewhere, without taking ownership or responsibility for its lifecycle. - Don’t use
@StateObject
in child views where the object is passed in from a parent. Use@ObservedObject
instead. - You should not always use
@StateObject
for all use cases, as it is meant for managing the lifecycle of the object, and doing so inappropriately can lead to inefficiencies or incorrect behavior.