Going over the SwiftUI observation system
Before we discuss the current SwiftUI observation system, let’s recap the SwiftUI observation system.
Before Xcode 15, nine property wrappers handled state and data updates in SwiftUI.
Let’s try to group them by app levels:
- Sub-View level:
@
Binding
,@Environment
- View level:
@State
,@Binding
,@
StateObject
,@Environment
- Business Logic level:
@
ObservableObject
,@Published
- App/Data level:
@AppStorage
,@
SceneStorage
,@EnvironementObject
The different levels give us an idea of the different roles of the different wrappers. Let’s touch on some of these wrappers to understand how the system works.
A local @State
property wrapper manages the state of primitive properties within the view. For example, whether a specific view is hidden, the number of available buttons, the current sorting method, and more are managed by this wrapper.
The reason why we use a @State
property wrapper is because SwiftUI views are immutable. This means that SwiftUI rebuilds the view each time a change occurs, but the @State
values don’t change between one rendering session and another.
The problem begins when we base our view on data model information. An example of this would be a bookstore app that displays a list of books from a local data file. In this case, our view must work with another data model object using the ObservableObject
protocol.
Let’s go over it now.
Conforming to the ObservableObject protocol
We can use the ObservableObject
protocol in conjunction with the @ObservedObject
property wrapper for classes that need to be observed.
Here’s an example of a UserData
class which becomes an @ObservedObject
property wrapper:
class UserData: ObservableObject { @Published var username = "Avi Tsadok" } struct ContentView: View { @ObservedObject var userData = UserData() var body: some View { Text("Welcome, \(userData.username)!") .padding() } }
There are three parts to implementing a data class observation:
- Conforming to
ObservableObject
: If we want a class to be observed in SwiftUI, it must conform to theObservableObject
protocol. This indicates to SwiftUI that any instance derived from this class can be observed in a view. - Adding the
@Published
property wrapper: When we mark a property with a@Published
property wrapper, SwiftUI creates a publisher and uses it inside the SwiftUI views. - Marking variables with the
@ObservedObject
property wrapper: The@ObservedObject
property wrapper establishes a connection between the view and the object, allowing the view to be notified of changes.
It’s essential to remember that the @ObservedObject
property wrapper is solely for observation purposes – this means that the view cannot modify the observed object properties directly.
If we want to change the observed object properties, we must use another property wrapper – @
StateObject
.
A @StateObject
property wrapper is similar to @State
, only that it works with observable objects and not primitive values.
However, that doesn’t end here – if we want to create a two-way connection between the view and its subview, we need to add a @Binding
property wrapper to the subview and a @State
property wrapper to the parent view.
Explaining the problem with the current observation situation
The short recap of the current way observation works in SwiftUI emphasizes how complex and confusing it is to observe data in SwiftUI.
Take, for example, the ObservableObject
protocol – in most cases, we want to mark all of our properties with the @Published
property wrapper. If that’s the case, why do we need to work hard? Don’t we have a way to add a @Published
property wrapper to all our properties?
The observation framework uses Swift macros here, a feature that can help us reduce boilerplate code. To read more about it, go to Chapter 10 and read about Swift macros.