SwiftUI @State @Published @ObservedObject 深入理解與使用

SwiftUI @State @Published @ObservedObject 深入理解與使用

SwiftUI 是 Apple 新出面向未來、跨多端解決方案、聲明式程式設計

最後更新 2021/10/18 下午4:51
Renew全栈工程师
預計閱讀 6 分鐘
分類
Swift UI
標籤
ObservedObject Published State

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(父物件)裡面的隨便一個屬性
繼續探索

延伸閱讀

更多文章