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:
- TreeCounterViewModel - An ObservableObject hold the number and updates the view.
- 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:
- 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.
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 :).
Comments powered by Disqus.