SwiftUI 探索 - 狀態和數據流

SwiftUIiOS13新出的聲明式UI框架,將會徹底改變之前命令式操做UI的開發方式。此文章主要介紹SwiftUI中狀態管理的方式。swift

可變狀態

@State

ReactFlutter中的State相似,只不過ReactFlutter中須要顯式調用setState方法。在SwiftUI 中直接修改State屬性值,就觸發視圖更新。框架

由於State是使用了@propertyDelegate修飾的屬性值,其內部實現應該是在狀態值set方法中進行變動視圖的操做。異步

class Model: BindableObject {
    var didChange = PassthroughSubject<Model, Never>()
    var count: Int = 0 {
        didSet {
            didChange.send(self)// 調用didChange觸發變動操做
        }
    }
}
struct ContentView: View {
   @State private var text: String = "a"// 使用@State修飾
   @State private var model = Model()// 使用@State修飾
    var body: some View {
        VStack {
          Text(text)
          Text(model.count)
          Button(action: {
              self.text = "b"// 修改text會更新視圖
              self.count += 1
            }) {
              Text("update-text")
          }
        }
    }
}
複製代碼
  • 若是想使用class類型屬性做爲State屬性,類對象須要實現BindableObject協議。當調用didChangesend方法時,會通知關聯的View更新視圖。didChangePublisher(新出的Combine異步事件處理框架,相似RxSwift)類型,調用send時會發送一個新的值給訂閱者。
  • 當修改的State屬性值沒有在body中使用或者修改後的State屬性值和上一次相同,並不會觸發從新計算body

State屬性修改時,會檢測State屬性被使用和檢測值變動來決定要不要更新視圖和觸發body方法。性能

  • State屬性用class類型。在觸發body從新計算前會檢查State值有沒有改變,當修改類對象屬性時,由於類對象指針並無改變,因此並不會觸發視圖更新。若是想觸發視圖變動,能夠在修改State時生成新的對象(這種方式不太好)或者使用BindableObject

屬性

Property

React中的Props相似,用於父視圖向子視圖傳遞值。ui

struct PropertyView: View {
    let text: String// 當text改變時,會從新計算`body`。
    var body: some View {
        Text(text)
    }
}
struct ContentView: View {
    var body: some View {
        PropertyView(text: "a")
    }
}
複製代碼
  • 使用let變量。使用var變量修飾屬性,在body方法裏也不能修改,由於修改屬性會建立新的結構體。

@Binding

Property功能相似,用於父視圖向子視圖傳遞值。只不過Binding屬性能夠修改,修改Binding屬性會觸發父視圖State改變從新計算body。能夠實現反向數據流的功能,有點相似MVVM的雙向綁定。spa

struct BindingView : View {
    @Binding var text: String // 使用@Binding修飾
    var body: some View {
        VStack {
            Button(action: {
                self.text = "b"
            }) {
                Text("update-text")
            }
        }
    }
}
struct ContentView : View {
    @State private var text: String = "a" // State
    var body: some View {
        VStack {
            BindingView(text: $text)// State變量使用$獲取Binding
            Text(text)
        }
    }
}
複製代碼

@ObjectBinding

@ObjectBinding彷佛和State類似,暫時不太清楚使用上有什麼區別。@State替換@ObjectBinding使用沒有問題,@Binding替換@ObjectBinding使用也沒有問題。雙向綁定

class Model: BindableObject {
    var didChange = PassthroughSubject<Model, Never>()
    var count: Int = 0 {
        didSet {
            didChange.send(self)
        }
    }
}
struct ChildView: View {
// @Binding var model: Model
// @ObjectBinding var model: Model
    var body: some View {
        VStack {
            Text("count2-\(model.count)")
            Button(action: {
                self.model.count += 1
            }) {
                Text("update")
            }
        }
    }
}

struct ContentView : View {
// @State private var model = Model()
// @ObjectBinding private var model = Model()
    var body: some View {
        VStack {
            ChildView(model: model)
            Text("count1-\(model.count)")
        }
    }
}
複製代碼

上面StateObjectBindingBinding註釋的地方任意使用結果都同樣,視圖能正確更新。指針

@EnvironmentObject

經過Property或者Binding的方式,咱們只能顯式的經過組件樹逐層傳遞。code

顯式逐層傳遞的缺點對象

  • 當組件樹複雜的時候特別繁瑣,修改起來也很麻煩。
  • 有些屬性在視圖樹中間的層級不會使用到,只有底層會使用。會增長中間層級視圖的複雜度。也能夠避免中間的層級重複計算body觸發視圖更新。

爲了不層層傳遞屬性,可使用Environment變量。Environment屬性能夠在任意子視圖獲取並使用。和React中的Context很類似。

struct EnvironmentView1: View {
    var body: some View {
        return VStack {
            EnvironmentView2()
            EnvironmentView3()
        }
    }
}
struct EnvironmentView2: View {
    @EnvironmentObject var model: Model// 使用@EnvironmentObject修飾屬性
    var body: some View {
       Button(action: {
                self.model.change()
            }) {
                Text("update-Environment")
            }
    }
}
struct EnvironmentView3: View {
    @EnvironmentObject var model: Model// EnvironmentObject
    var body: some View {
        Text(model.text)
    }
}
struct ContentView: View {
    var body: some View {
        //EnvironmentObject須要使用environmentObject方法注入到組件樹中
        EnvironmentView1().environmentObject(Model())
    }
}
複製代碼
  • 經過environmentObject方法注入對象到組件樹中,子組件樹中共享同一個對象而且能夠監聽變動。
  • @EnvironmentObject查找如何能獲取到對應的對象,大概是根據屬性的類型進行查找,因此多個屬性只要類型相同,就能取到一樣的對象。當組件樹有多個組件使用environmentObject方法注入同類型的對象時,獲取時會查找最近的父組件的對象。

目前好像沒有方式實現根據不一樣的key來注入多個對象並獲取。

數據流

父視圖 -> 子視圖向下傳遞

  • 不須要修改使用Property
  • 須要修改使用@Binding

父視圖 -> 子視圖跨層級向下傳遞

  • Environment

全局狀態層管理

  • 應該是結合Combine框架根據模塊功能領域分層進行管理。

視圖更新流程

  • 修改State觸發視圖更新,檢測State是否被使用以及值是否被改變。
  • 從新計算body生成新的視圖樹,會從新建立全部子視圖的View結構體。
  • 遍歷全部子視圖,判斷View結構體與更新前是否一致。當不一致時,觸發子視圖更新,調用子視圖body

Tips

關於 State

class Model: BindableObject {
    var didChange = PassthroughSubject<Model, Never>()
    var count: Int = 0 {
        didSet {
            didChange.send(self)
        }
    }
    init() {
        print("Model-init-\(count)")// 這裏count始終爲0
    }
}
struct Struct {
    private(set) var count = 0
    init() {
        print("Struct-\(count)")// 這裏count始終爲0
    }
    mutating func update() {
        print("update-\(count)")
        count += 1
    }
}
struct ChildView: View {
    @State private var model2 = Struct()
    @State private var model = Model2()
    @State private var count = 0
    var body: some View {
        return VStack {
            Text("\(model.count)")
            Text("\(model2.count)")
            Text("\(count)")
            Button(action: {// 修改 State
                self.model.count += 1
                self.count += 1
                self.model2.update()
            }) {
                Text("update")
            }
            }
        }
}
struct ContentView: View {
    @State private var count = 0
    var body: some View {
        return VStack {
            ChildView()
            Button(action: {
                self.count += 1
            }) {
                Text("update")
            }
            Text("\(count)")
        }
    }
}
複製代碼
  • ContentView更新時,會從新建立ChildView結構體。
  • ChildView中的State都會從新建立,StructModel初始化方法中,count一直爲0,即便ContentViewState曾經修改過。可是下一次修改State值時,State會使用以前的值作運算。

不太清楚這裏是如何處理的,State雖然從新初始化了一次,彷佛仍是使用的以前的State

  1. 例如當點擊Button時,會修改ChildViewmodel, model2count+=1,當前count=1。
  2. ChildView從新建立時,modelmodel2初始化方法中,count=0。
  3. 當下一次點擊Button修改count值時,count會在1的基礎上+1,以後count=2。

性能

  • 當視圖發生變動時,因爲body會常常從新計算,因此應該儘可能避免在body中進行重複和耗時計算。
  • 視圖變動時,視圖組件View結構體會從新建立,因此應該避免在init方法中進行重複和耗時計算。(包括屬性的從新生成)
  • 根據上面 State的特性,當State屬性爲結構體或類時,應避免在init方法中訪問或修改屬性。由於當State修改事後,在init方法中獲取到的值不是正確的,修改值也會生效。
相關文章
相關標籤/搜索