- SwiftUI is Apple's new, forward-looking, cross-platform, declarative programming framework.
The latest version of SwiftUI is 2.0, but it requires iOS 14 support. Many are still using iOS 13, so many incomplete features are replaced by SwiftUIX and various libraries, and bugs are endless.
- Below is my understanding of @State, @Published, and @ObservedObject. Please point out if I'm wrong.
1. Introduction to @State
Because SwiftUI View uses structs, when we create methods for a struct that want to change its properties, we need to add the mutating keyword, for example:
mutating func doSomeWork()
However, Swift does not allow us to create mutable computed properties, which means we cannot write mutating var body: some View — it's not allowed.
@State allows us to bypass the restriction of structs: we know we can't change their properties because structs are immutable, but @State allows SwiftUI to store that value separately in a place that can be modified.
Yes, this feels a bit like cheating, and you might wonder why we don't just use classes — they can be freely modified. But trust me, it's worth it: as you progress, you'll learn that SwiftUI frequently destroys and recreates your structs, so keeping them small and simple is important for performance.
Tip: There are several ways to store program state in SwiftUI, and you will learn all of them. @State is specifically designed for simple properties that are stored in a single view. Therefore, Apple recommends adding private access control to these properties, like: @State private var tapCount = 0.
2. Introduction to @Published + @ObservedObject
@Published is one of SwiftUI's most useful wrappers, allowing us to create object properties that can be automatically observed. SwiftUI automatically monitors this property, and once it changes, it automatically updates the interface bound to that property.
For example, our defined data structure Model, provided that @Published is used under ObservableObject. Then we use @ObservedObject to reference this object. Of course, @State wouldn't cause a compile error, but it would not update the view.
class BaseModel: ObservableObject{
@Published var name:String = ""
}
struct ContentView: View{
@ObservedObject var baseModel:BaseModel = BaseModel()
var body: some View{
Text("User name: \(baseModel.name)")
Button(action: {
baseModel.name = "Renew"
}, label: {
Text("Update View")
})
}
}
3. The Most Important Part (The code comments are the most crucial, be sure to read them fully)
Although the above example works normally and displays correctly, when it comes to actual projects, there are a ton of bugs. How does this happen? If you don’t understand the binding relationship between these three states and the View, you might leave hidden problems for yourself.
First, take a look at the following examples
//// MASK - First define two Models that inherit ObservableObject
class WorkModel: ObservableObject {
@Published var name = "name"
@Published var count = 1
}
class UserModel: ObservableObject {
@Published var nickname = "nickname"
@Published var header = "http://www.baidu.com"
}
//// MASK - View display layer
struct ContentView: View {
@ObservedObject var workModel:WorkModel = WorkModel()
@ObservedObject var userModel:UserModel = UserModel()
var body: some View {
VStack{
Text("work.count \(workModel.count)")
Text("work.name \(workModel.name)")
Text("user.nickname \(userModel.nickname)")
Text("user.header \(userModel.header)")
Button(action: {
userModel.nickname = "Renew"
userModel.header = "http://..."
workModel.name = "work name"
workModel.count += 1
}, label: {
Text("Update Data")
})
}
}
}
As expected, the above code will update the data when the button is clicked. But what if we have a wrapper class?
class WrapperModel: ObservableObject{
@ObservedObject var workModel:WorkModel = WorkModel()
@ObservedObject var userModel:UserModel = UserModel()
}
struct ContentView: View {
@ObservedObject var wrapperModel:WrapperModel = WrapperModel()
var body: some View {
VStack{
Text("work.count \(wrapperModel.workModel.count)")
Text("work.name \(wrapperModel.workModel.name)")
Text("user.nickname \(wrapperModel.userModel.nickname)")
Text("work.header \(wrapperModel.userModel.header)")
Button(action: {
wrapperModel.userModel.nickname = "Renew"
wrapperModel.userModel.header = "http://..."
wrapperModel.workModel.name = "work name"
wrapperModel.workModel.count += 1
}, label: {
Text("Update Data")
})
}
}
}
Will the button click still update the data now? The answer is no. Why is that?
Because SwiftUI's update mechanism requires the first-level bound object's properties (fields) to change in order to trigger the view update.
But what if the first-level object also contains @ObservedObject (or other types of objects) that are bound?
Will it still trigger the first-level object's property update? The answer is no.
You can catch it in the didSet event, but it won't be triggered, so the view will not update. Are there any other solutions?
Yes:
Call the wrapperModel.objectWillChange.send() method to tell the View layer that I have updated. But is this absolutely reliable? No. If the model has deeper nesting, there are still bugs and it won't trigger.
4. Summary and Solution
/// Since we know that the binding relationship between View and state
/// is based on the update of properties (fields) under the first-level class that inherits ObservableObject,
/// we can add an irrelevant field to ObservableObject and write a method to notify the update.
class BaseobservableObject: ObservableObject {
///
/// Note:
/// When receiving subclass models, you must use @ObservedObject instead of @Published,
/// because SwiftUI's update mechanism is that when a @Published field of the current object changes,
/// it triggers the View to update.
/// In BaseModel, implement notifyUpdate to update the _lastUpdateTime field of the current object,
/// thus updating all fields of the object itself.
@Published private var _lastUpdateTime: Date = Date()
///
/// Notify update
public func notifyUpdate() {
_lastUpdateTime = Date()
}
}
/// Then, when the object under the wrapper class is updated,
/// we can directly call the wrapper's notifyUpdate() method to update the current object's properties,
/// thereby achieving the effect of updating the View.
/// Concern: If notifyUpdate() is called multiple times, will the View refresh twice?
/// The answer is no. If notifyUpdate() is called multiple times within the same function call stack,
/// the View will only update once.
/// When a subclass inherits from BaseobservableObject,
/// the properties under that object no longer need to be marked with @ObservedObject or @Published,
/// because after updating the property, calling notifyUpdate() achieves the effect of updating the entire object,
/// so they can be omitted.
5. Additional Knowledge
/// MASK - Implement a base Model class, and other Model classes inherit from it.
class BaseModel: ObservableObject {
@Published var isLoading = false
}
class SonModel: BaseModel {
@Published var name = "name"
@Published var count = 1
}
struct ContentView: View {
@ObservedObject var sonModel:SonModel = SonModel()
var body: some View {
VStack{
Text("name \(sonModel.name)")
Button(action: {
sonModel.name = "Renew"
}, label: {
Text("Load")
})
}
}
}
/// Here's the problem: Now the View layer directly references SonModel.
/// Logically, the Text should display "name Renew", but clicking doesn't respond.
/// Why? The issue is still somewhat similar to the problem above.
/// SonModel does not directly inherit from ObservableObject (it inherits from BaseModel which inherits from ObservableObject).
/// Therefore, if the properties (fields) of the directly inherited ObservableObject (the parent object) are not updated,
/// the View won't update.
/// The simplest solution is to update any property of the directly inherited ObservableObject (the parent object).