SwiftUI @StateObject Vs @ObservedObject

Roopesh Tripathi

--

@StateObject or @ObservedObject primarily depends on how you want to manage the lifecycle of the data model within your SwiftUI views. Here’s a closer look at both, especially in API-calling scenarios, and detailed examples to illustrate their best uses.

1. Overview of @StateObject vs. @ObservedObject

  • @StateObject: This property wrapper should be used when a view is responsible for creating and owning the lifecycle of an observable object. The instance created @StateObject persists across view re-renderings, so the data will not reset each time the view updates.
  • @ObservedObject: Use this when a view receives an observable object from an external source (such as a parent view) and needs to observe its changes without owning its lifecycle. @ObservedObject will not initialize or persist the object; it simply observes the existing object.

2. When to Use @StateObject with API Calls

The preferred option is when your view initiates a data model that fetches data from an API and manages states (e.g., loading, success, failure). This ensures the model is created once and persists even if the view updates due to a state change.

  • Example Use Case: A view that initiates a network request on load, and manages the state of the response, like a list of items.

Example: Fetching API Data with @StateObject

In this example, ContentView creates and owns DataModel, which is responsible for fetching data from an API. Using @StateObject ensures DataModel only initializes once and retains its state.

import SwiftUI
import Combine

// Define the data model that performs the API call
class DataModel: ObservableObject {
@Published var items: [String] = []
@Published var isLoading = false

func fetchData() {
isLoading = true
// Simulate API call
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
DispatchQueue.main.async {
self.items = ["Item 1", "Item 2", "Item 3"]
self.isLoading = false
}
}
}
}

struct ContentView: View {
@StateObject private var dataModel = DataModel() // Using @StateObject

var body: some View {
VStack {
if dataModel.isLoading {
ProgressView("Loading...")
} else {
List(dataModel.items, id: \.self) { item in
Text(item)
}
}
}
.onAppear {
dataModel.fetchData()
}
}
}

Why @StateObject?

  • @StateObject ensures dataModel is created only once when ContentView is initialized.
  • The view updates when dataModel.items or dataModel.isLoading changes, but the model persists across re-renders.

3. When to Use @ObservedObject with API Calls

@ObservedObject is better when a parent view or external source manages the object’s lifecycle, and the child view just needs to observe changes without owning or initializing it.

  • Example Use Case: A parent view (e.g., MainView) owns the data model and passes it to child views to observe.

Example: Passing API Data with @ObservedObject

In this scenario, MainView manages DataModel, and ChildView observes it using @ObservedObject.

struct MainView: View {
@StateObject private var dataModel = DataModel() // Initialize in parent view

var body: some View {
VStack {
Text("Main View")
ChildView(dataModel: dataModel) // Pass model to child view
}
.onAppear {
dataModel.fetchData()
}
}
}

struct ChildView: View {
@ObservedObject var dataModel: DataModel // Observe without creating

var body: some View {
List(dataModel.items, id: \.self) { item in
Text(item)
}
}
}

Why @ObservedObject?

  • ChildView observes changes in dataModel without owning its lifecycle.
  • The model’s lifecycle is managed by MainView, making ChildView lighter and more reusable.

4. When to Avoid Using @StateObject and @ObservedObject

  • Avoid @StateObject in nested or child views if the parent already initializes the object. This can lead to multiple instances of the same data model, which can be inefficient and introduce bugs.
  • Avoid @ObservedObject if the view itself initializes the data model. This can cause data loss on view re-renders since @ObservedObject doesn’t persist the data model across view updates.

Example: Potential Issue with Multiple Instances

If both parent and child views are used @StateObject for the same data model, each will create a separate instance, leading to unexpected behaviour.

struct ParentView: View {
@StateObject private var dataModel = DataModel() // Correct use

var body: some View {
ChildView(dataModel: dataModel) // Pass it down to child
}
}

struct IncorrectChildView: View {
@StateObject private var dataModel = DataModel() // Incorrect: new instance created here

var body: some View {
List(dataModel.items, id: \.self) { item in
Text(item)
}
}
}

In the above example, ParentView and IncorrectChildView both instantiate DataModel using @StateObject, leading to multiple instances that don’t share data, which can create bugs and performance issues.

Summary

  • Use @StateObject when the view owns the observable object and manages its lifecycle.
  • Use @ObservedObject when the view receives an observable object from an external source, such as a parent view, and only needs to observe changes.
  • Avoid multiple instances: Ensure only one view in a view hierarchy owns an instance of the data model to prevent duplication and unexpected behaviour.

--

--

No responses yet