SwiftUI
是iOS13
新出的聲明式UI框架,將會徹底改變之前命令式
操做UI的開發方式。此文章主要介紹SwiftUI
中狀態管理的方式。swift
與React
和Flutter
中的State
相似,只不過React
和Flutter
中須要顯式調用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
協議。當調用didChange
的send
方法時,會通知關聯的View
更新視圖。didChange
是Publisher
(新出的Combine
異步事件處理框架,相似RxSwift
)類型,調用send
時會發送一個新的值給訂閱者。State
屬性值沒有在body
中使用或者修改後的State
屬性值和上一次相同,並不會觸發從新計算body
。
State
屬性修改時,會檢測State
屬性被使用和檢測值變動來決定要不要更新視圖和觸發body
方法。性能
State
屬性用class
類型。在觸發body
從新計算前會檢查State
值有沒有改變,當修改類對象屬性時,由於類對象指針並無改變,因此並不會觸發視圖更新。若是想觸發視圖變動,能夠在修改State
時生成新的對象(這種方式不太好)或者使用BindableObject
。與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
方法裏也不能修改,由於修改屬性會建立新的結構體。與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
彷佛和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)")
}
}
}
複製代碼
上面
State
,ObjectBinding
,Binding
註釋的地方任意使用結果都同樣,視圖能正確更新。指針
經過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
Combine
框架根據模塊功能領域分層進行管理。State
觸發視圖更新,檢測State
是否被使用以及值是否被改變。body
生成新的視圖樹,會從新建立全部子視圖的View
結構體。View
結構體與更新前是否一致。當不一致時,觸發子視圖更新,調用子視圖body
。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
都會從新建立,Struct
和Model
初始化方法中,count
一直爲0
,即便ContentView
裏State
曾經修改過。可是下一次修改State
值時,State
會使用以前的值作運算。不太清楚這裏是如何處理的,
State
雖然從新初始化了一次,彷佛仍是使用的以前的State
。
- 例如當點擊Button時,會修改
ChildView
中model
,model2
中count
+=1,當前count
=1。- 當
ChildView
從新建立時,model
,model2
初始化方法中,count
=0。- 當下一次點擊Button修改
count
值時,count
會在1的基礎上+1,以後count
=2。
body
會常常從新計算,因此應該儘可能避免在body
中進行重複和耗時計算。View
結構體會從新建立,因此應該避免在init
方法中進行重複和耗時計算。(包括屬性的從新生成)State
的特性,當State
屬性爲結構體或類時,應避免在init
方法中訪問或修改屬性。由於當State修改事後,在init
方法中獲取到的值不是正確的,修改值也會生效。