Home SwiftUI - Observed vs State Object
Post
Cancel

SwiftUI - Observed vs State Object

One of the main features of reactive programming is observable objects. SwiftUI provides the feel of reactive programming by property wrappers like StateObject and ObservedObject. Both property wrappers notify the SwiftUI View of any state change.

Confusion is about which one to use at a given time. Letโ€™s check the basics first.

Basics

StateObject & ObservedObject require conformance to the ObservableObject protocol. It (ObservableObject) is a type with a publisher that emits before an object has been changed. Only the reference type (Class) can implement this because the tracking state change behaviour was impossible with the value type.

Till now it seems both have similar behaviour. Letโ€™s try to use one by one in an application.

๐ŸŒ“ Talk is cheap. Show me the code

Suppose you are travelling in a bus and sitting on the window seat. You started to count trees on the roadside. A favourite time passed in childhood. I have difficulty remembering numbers, so I created a small application:

It has two components:

  1. TreeCounterViewModel - An ObservableObject hold the number and updates the view.
  2. TreeCounterView - Display the total count.

@ObservedObject

1
2
3
4
5
6
7
class TreeCounterViewModel: ObservableObject {
    @Published var treeCount = 0
    
    func addTree() {
        self.treeCount += 1
    }
}

The treeCount being @Published property forces the view to redraw.

1
2
3
4
5
6
7
8
9
10
11
12
struct TreeCounterView: View {
    @ObservedObject var viewModel = TreeCounterViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Number of Trees: \(self.$viewModel.treeCount.wrappedValue)")
            Button("Add Trees") {
                self.viewModel.addTree()
            }
        }
    }
}

The result is here:

@StateObject

Letโ€™s try to change the ViewModel to a @StateObject.

1
2
3
4
5
6
7
8
9
10
11
12
struct TreeCounterView: View {
    @StateObject var viewModel = TreeCounterViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Number of Trees: \(self.$viewModel.treeCount.wrappedValue)")
            Button("Add Trees") {
                self.viewModel.addTree()
            }
        }
    }
}

Oh, No difference in the result:

In both cases, Tree Counter is working as expected. So why two property wrappers with the same behaviour?

I thought to enhance the application by capturing total travel time.

When to use State Object

So, In the application one new component has been added:

  1. TravelTimePassView - Start & display the time elapsed in travel, also give an option to count trees i.e. TreeCounterView is added as a child.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct TravelTimePassView: View {
    @State private var timer = Timer.publish(every: 1, on: .main, in: .common)
    @State private var timerCancel: Cancellable!
    @State private var timeElapsed: TimeInterval = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("Travel Time Elapsed: \(self.$timeElapsed.wrappedValue)")
            HStack (spacing: 20) {
                Button("Start Travel") {
                    self.startTravelTime()
                }
                Button("Travel Complete", action: {
                    self.travelComplete()
                })
            }
            Spacer().frame(height: 40)

            TreeCounterView() // Init Tree Counter View
        }.onReceive(timer) { _ in
            self.timeElapsed += 1
        }
    }
    
    fileprivate func startTravelTime() {
        self.timerCancel?.cancel()
        self.timer = Timer.publish(every: 1, on: .main, in: .common)
        self.timerCancel = self.timer.connect()
    }
    
    fileprivate func travelComplete() {
        self.timerCancel?.cancel()
        self.timeElapsed = 0
    }
}

Timer
I have used a timer which will trigger every second. It will start by pressing the Start Travel button and stop by pressing the Travel Complete button.

Timer Cancel
A variable to hold timer cancellable type and it stops the running timer.

Time Elapsed
I used a counter to count the total seconds that elapsed from the timerโ€™s start.

TreeCounterView
You can observe in the body of TravelTimePassView that TreeCounterView is initiated every time the body renders.

Now letโ€™s try TreeCounterView again with View Model property wrapper @StateObject & ObservedObject.

@StateObject var viewModel@ObservedObject var viewModel

Now we can see the difference clearly, So SwiftUI manages the lifecycle differently for State & Observed Object.

If the Observed Object is initiated within view then it will be reinitialized when the view is recreated. On the other hand, State Object is unchanged on recreation of view.

๐ŸŒ“ Wind Up

When a SwiftUI view needs to react to changes in data managed by an existing object (often a view model) thatโ€™s created and controlled outside the view hierarchy then @observedObject property wrapper can be used. In short, it should only be used through Dependency Injection.

Use @StateObject to ensure consistent results after View Redraw. However, if the same state object needs to be passed to subsequent views, it should be marked as @observedObject in subsequent Views.

Thanks for reading! Iโ€™d love to hear your thoughts on this article :).

This post is licensed under CC BY 4.0 by the author.

John's Interview - Loader Design

Property Wrappers in Swift - (Must Use Feature)

Comments powered by Disqus.