SwiftUI @StateObject Vs @ObservedObject
@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
ensuresdataModel
is created only once whenContentView
is initialized.- The view updates when
dataModel.items
ordataModel.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 indataModel
without owning its lifecycle.- The model’s lifecycle is managed by
MainView
, makingChildView
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.