ObservableObject protocol @StateObject part 1

abdul ahad
7 min readSep 30, 2024
Photo by Nadi Whatisdelirium on Unsplash

What is @StateObject?

In SwiftUI, @StateObject is used to create and manage an instance of a class that conforms to the ObservableObject protocol. This property wrapper ensures that the object is persisted across view updates, and that the view will re-render when properties within the object change.

  • @StateObject is designed for objects that are owned by the view, meaning that the view is responsible for the lifecycle of the object.
  • When the object changes (because of @Published properties), the SwiftUI view will automatically re-render.

Key Concepts of @StateObject:

Used for Observable Objects:

  • The object being managed must conform to the ObservableObject protocol.
  • This protocol requires the object to have an objectWillChange publisher, which notifies SwiftUI before any changes occur in the object’s properties.

Persists Across View Updates:

  • The object managed by @StateObject persists across view updates and is not recreated when the view is re-rendered.
  • This differs from @State, which is used for simple value types (e.g., Int, Bool, String) rather than objects.

ObservableObject Protocol and @Published Properties:

To make an object observable, it needs to conform to the ObservableObject protocol. Typically, the object will have properties marked with the @Published property wrapper, which ensures changes to these properties will trigger the objectWillChange publisher and notify SwiftUI to update the view.

Here’s an example:

final class Model: ObservableObject {
@Published var value = 0
}

In the example above:

  • The Model class conforms to ObservableObject, and its value property is marked as @Published.
  • Whenever value changes, the view will be notified and will re-render.

How @StateObject Works in SwiftUI:

Let’s use the following example to illustrate the role of @StateObject in a SwiftUI view:

struct Counter: View {
@StateObject private var model = Model() // Create and manage an observable object

var body: some View {
Button("Increment: \(model.value)") {
model.value += 1 // Increment the value
}
}
}

Creating the Object:

  • The @StateObject property wrapper creates an instance of Model, and SwiftUI keeps track of this object for the lifetime of the view.
  • The model object is owned by the Counter view, and SwiftUI manages its lifecycle.

ObservableObject and View Re-renders:

  • The model.value property is used inside the Button label, so SwiftUI tracks that the view depends on model.value.
  • When the button is pressed, model.value is incremented, which triggers the objectWillChange publisher and notifies SwiftUI to re-render the view.
  • SwiftUI re-renders the view to reflect the new value of model.value.

Behind the Scenes of @StateObject:

When you declare @StateObject, SwiftUI automatically generates some additional code to manage the object's state and track its changes.

Without Using Property Wrapper Syntax:

The @StateObject property wrapper is syntactic sugar, and without it, the code would look like this:

struct Counter: View {
private var _model = StateObject(wrappedValue: Model())
private var model: Model { _model.wrappedValue }

var body: some View {
Button("Increment: \(model.value)") {
model.value += 1
}
}
}
  • _model: This is the underscored variable that holds the StateObject instance, managing the lifecycle of the observable object.
  • model: This is a computed property that accesses the wrappedValue of the StateObject, which is the actual Model object.

objectWillChange Publisher:

The ObservableObject protocol requires the class to implement the objectWillChange publisher, which informs SwiftUI about changes before they happen.

  • By using @Published, you automatically trigger the objectWillChange publisher whenever a property marked with @Published changes.
  • If you want to manually trigger the publisher, you can override the willSet property observer to call objectWillChange.send().
final class Model: ObservableObject {
var value = 0 {
willSet {
objectWillChange.send() // Notify SwiftUI that a change will occur
}
}
}

This is the manual version of what @Published does behind the scenes.

The View Lifecycle and @StateObject:

Here’s a step-by-step breakdown of what happens when you use @StateObject:

Initialization:

  • The view is created, but the @StateObject instance (model) is not initialized yet.
  • The StateObject holds the initial value as an autoclosure, meaning it will create the object when the render tree node is created.

Object Creation:

  • When the render tree node (for the Counter view) is created, the StateObject is initialized, and the Model object is stored in the render tree.
  • The view uses this Model object to render itself.

View Re-Renders:

  • When the button in the view is tapped, model.value is updated.
  • The @Published property triggers objectWillChange, which notifies SwiftUI that the view needs to update.
  • SwiftUI re-executes the view’s body and updates the Button label to reflect the new value.

Persistence Across Re-renders:

  • The Model instance managed by @StateObject persists across view updates and is not re-created unless the view is completely removed and recreated.

Common Mistakes:

Passing Objects from Outside:

  • @StateObject should not be used for objects passed into the view from the outside (e.g., from a parent view). For externally passed objects, use @ObservedObject. This doesn’t work for the same reason when talking about @State: when the view’s initializer runs, the view doesn’t yet have identity. As a rule of thumb, we should only use @StateObject when we can assign an initial value to the property on the line where we declare the property,
  1. Wrong:
struct ParentView: View {
@StateObject var model: Model

var body: some View {
Counter(model: model) // Do not use @StateObject here
}
}

Correct:

struct Counter: View {
@ObservedObject var model: Model // Use @ObservedObject for externally managed objects
// ...
}

Initializing State in the Initializer:

  • Avoid creating a @StateObject in the view’s initializer. Instead, assign the initial value directly when declaring the property.

Wrong:

struct Counter: View {
@StateObject var model: Model

init() {
self.model = Model() // Do not initialize in the init method
}
}

Correct:

struct Counter: View {
@StateObject var model = Model() // Initialize when declaring
}

Example: Issues with Passing @stateobject in initializer

In this example, the goal is to create a counter that tracks different values for multiple rooms (e.g., Hallway, Living Room, Kitchen). Each room has its own separate CounterModel, which is responsible for holding the count for that specific room.

However with @StateObjectthe count value is shared across all rooms as you can see in the snapshot below.

import SwiftUI

// ObservableObject class
class CounterModel: ObservableObject {
@Published var value = 0
}

// External model manager
class Model {
static let shared = Model()
var counters: [String: CounterModel] = [:]

func counterModel(for room: String) -> CounterModel {
if let model = counters[room] {
return model
}
let newModel = CounterModel()
counters[room] = newModel
return newModel
}
}

// View that observes an external object
struct Counter: View {
@StateObject var model: CounterModel // Observing the passed model

var body: some View {
Button("Increment: \(model.value)") {
model.value += 1
}
}
}

// ContentView that passes the model to Counter
struct ContentView: View {
@State private var selectedRoom = "Hallway"

var body: some View {
VStack {
Picker("Room", selection: $selectedRoom) {
ForEach(["Hallway", "Living Room", "Kitchen"], id: \.self) { room in
Text(room).tag(room)
}
}
.pickerStyle(.segmented)

Counter(model: Model.shared.counterModel(for: selectedRoom))
}
}
}

Why the Same Value Is Shared with @StateObject:

The @StateObject property wrapper works much in the same way as @State: we specify an initial value (an object in this case), which will be used as the starting point when the node in the render tree is created. From then on, SwiftUI will keep this object around across rerenders for the lifetime of the node in the render tree.

Similarly to above in the example:

  1. @StateObject is designed to own and manage the lifecycle of an object: When you use @StateObject in a SwiftUI view, SwiftUI expects this view to create and maintain the lifecycle of the object. It assumes that this view is the only owner of the object.
  2. You are passing an object from the outside: In your case, you are passing an object from a shared external model (Model.shared.counterModel(for:)). So, each time the Counter view is re-rendered, @StateObject doesn't reinitialize the object as expected. Instead, it uses the already existing instance that’s being shared by the external model when the first time it was created as the object is kept around across rerenders. This causes all counters to point to the same object, so changing the value in one room’s counter will also affect the others and update at the same time.

Why This Doesn’t Happen with @ObservedObject:

for more details you can go to part 2

  1. @ObservedObject just observes an object without managing its lifecycle: When you use @ObservedObject, SwiftUI doesn’t assume that the view owns the object. It simply observes the object passed from the outside.
  2. Each room gets its own CounterModel: When you switch rooms and pass a new CounterModel instance from your shared Model, @ObservedObject will observe the new instance. This means that each CounterModel is distinct for each room, and changes in one room won’t affect the others.
  3. This is why @ObservedObject works in your case: it lets SwiftUI just observe the CounterModel provided by the external model, ensuring that each room’s counter remains independent.

Summary:

  • @StateObject is used to manage an observable object that is owned by a view. It persists across view re-renders and ensures the view updates when the object’s properties change.
  • ObservableObject is a protocol used to make a class observable by SwiftUI, typically with properties marked @Published.
  • Use @StateObject for objects that are private to the view and need to persist across updates, while @ObservedObject should be used for objects passed in from the outside.

References:

--

--

abdul ahad
abdul ahad

Written by abdul ahad

A software developer dreaming to reach the top and also passionate about sports and language learning

No responses yet