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
@ObservedObjectbecauseContentViewModelis reinitialized each timeScreen1is 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
@ObservedObjectreinitializesContentViewModeleach 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 (
@StateObjectinScreen1). - 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
@StateObjectensures that theContentViewModelis initialized only once, and its state is preserved across view updates.
Explanation of the Difference:
- With
@ObservedObject: Each timeScreen1is recreated (toggled in or out of the view hierarchy), theviewModelis reinitialized, causing the counter to reset to 0. - With
@StateObject: TheviewModelis 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
@StateObjectwhen the view creates and owns the object and is responsible for maintaining its lifecycle across view updates. - Use
@ObservedObjectwhen the view is observing an object passed from elsewhere, without taking ownership or responsibility for its lifecycle. - Don’t use
@StateObjectin child views where the object is passed in from a parent. Use@ObservedObjectinstead. - You should not always use
@StateObjectfor 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.
