1.SwiftUI 是 Apple 新推出的面向未來、跨多端解決方案、宣告式程式設計
SwiftUI 最新版本 2.0 但是需要 iOS 14 支援,多數現在還用的是 iOS 13 所以很多不完善的東西都用 SwiftUIX 以及各種庫代替,bug 也是層出不窮
2.下面是鄙人對 @State @Published @ObservedObject 的理解,如有不對,還請指出
1.@State 介紹
因為 SwiftUI View 採用的是結構體,當創建想要更改屬性的結構體方法時,我們需要添加 mutating 關鍵字,例如:
mutating func doSomeWork()
然而,Swift 不允許我們創建可變計算屬性,這意味著我們不能編寫 mutating var body: some View——這是不允許的。
@State 允許我們繞過結構體的限制:我們知道不能更改它們的屬性,因為結構是固定的,但是 @State 允許 SwiftUI 將該值單獨存儲在可以修改的地方。
是的,這感覺有點像作弊,你可能想知道為什麼我們不使用類——它們可以自由修改。但是相信我,這是值得的:隨著你的進步,你會了解到 SwiftUI 經常破壞和重新創建你的結構體,所以保持它們小而簡單的結構對性能很重要。
提示:在 SwiftUI 中存儲程式狀態有幾種方法,你將學習所有這些方法。@State 是專門為存儲在一個視圖中的簡單屬性而設計的。因此,蘋果建議我們向這些屬性添加私有訪問控制,比如:@State private var tapCount = 0。
2.@Published + @ObservedObject 介紹
@Published 是 SwiftUI 最有用的包裝之一,允許我們創建出能夠被自動觀察的物件屬性,SwiftUI 會自動監視這個屬性,一旦發生了改變,會自動修改與該屬性綁定的界面。
比如我們定義的資料結構 Model,前提是 @Published 要在 ObservableObject 下使用 然後用 @ObservedObject 來引用這個物件,當然 @State 不會報錯,但是無法更新
class BaseModel: ObservableObject{
@Published var name:String = ""
}
struct ContentView: View{
@ObservedObject var baseModel:BaseModel = BaseModel()
var body: some View{
Text("用戶名\(baseModel.name)")
Button(action: {
baseModel.name = "Renew"
}, label: {
Text("更新視圖")
})
}
}
3.最重要的部分 (程式碼註釋部分最為主要,務必看完)
雖然上面案例運行中什麼都正常展示加載,但是到了實際專案中,卻一堆 bug,這是如何導致的,如果對這三種狀態跟 View 綁定的關係不了解,很可能給自己留下隱患
先來看組案例
//// MASK - 先定義兩個 Model 繼承 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 顯示層
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("更新數據")
})
}
}
}
不出意外上面程式碼點擊按鈕就會更新數據,但是如果我們有個包裝類呢
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("更新數據")
})
}
}
}
這時候點擊按鈕還會更新數據嗎,答案是否定的,那這個是為啥呀???
因為 SwiftUI 更新數據的前提是觸發
第一層 綁定的物件 wrapperModel 下的屬性(欄位)發生更新才會調用視圖層更新數據
但是 第一層下綁定的物件還綁定了 @ObservedObject 或者其他類型的物件呢?
還會觸發第一層物件屬性更新嗎,答案是不能的
你可以在 didSet 事件裡面捕捉,是捕捉不到的,所以視圖是不會更新的,那這還有其他解決方案嗎
有:
調用物件 wrapperModel.objectWillChange.send() 方法告訴 View 層我更新 但是這個就是絕對的了吗?:不是 如果層次再深一點的 model 還是有 bug,觸發不了
4.總結以及解決方案
/// 既然我們知道 View 跟狀態綁定的關係
/// 是以第一繼承 ObservableObject 類下的屬性(欄位)更新來更新視圖的
/// 那我們可以給 ObservableObject 加一個無關緊要的欄位,然後編寫一個方法,來通知更新
class BaseobservableObject: ObservableObject {
///
/// 注意
/// 接收子類 model 時候要用 @ObservedObject 不能用 @Published
/// 因為 SwiftUI 更新機制是當前物件有 @Published 欄位更新就會調用 View 視圖進行更新
/// 在 BaseModel 裡面實現 notifyUpdate 更新當前物件 _lastUpdateTime 欄位,實現自身全部欄位更新
@Published private var _lastUpdateTime: Date = Date()
///
/// 通知更新
public func notifyUpdate() {
_lastUpdateTime = Date()
}
}
/// 那當我們包裝類下的物件更新的時候
/// 可以直接調用包裝類 notifyUpdate() 方法更新當前物件屬性,來達到更新 View 的效果
/// 顧忌:如果多次調用 notifyUpdate() View 會刷新兩邊嗎
/// 答案是否定的,在一次函式棧裡面多次調用 notifyUpdate() View 也只更新一次
/// 當子類繼承了 BaseobservableObject 物件
/// 那麼該物件下面屬性其實可以不需要再寫 @ObservedObject 或者 @Published 了
/// 因為更新屬性之後調用了 notifyUpdate() 達到了更新整個物件的效果,所以可以省略了
5.其他知識
/// MASK - 實現一個基礎 Model 類,其他 Model 繼承該類
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("加載")
})
}
}
}
/// 問題來了,現在我 View 層我直接引用
/// 照說這時候應該 Text 提示訊息是 name Renew 但是點擊沒反應
/// 啥原因,問題其實還是跟上面的問題有點相似
/// SonModel 不是直接繼承於 ObservableObject 類的
/// 所以,直接繼承 ObservableObject 下的屬性(欄位)沒更新,就不會更新 View
/// 最簡單的解決辦法就是更新直接繼承 ObservableObject(父物件)裡面的隨便一個屬性