SwiftUI
是iOS13
新出的聲明式UI框架捧搞,將會完全改變以前命令式
操作UI的開發(fā)方式砰琢。此文章主要介紹SwiftUI
中狀態(tài)管理的方式芙贫。
可變狀態(tài)
@State
與React
和Flutter
中的State
類似落萎,只不過React
和Flutter
中需要顯式調用setState
方法。在SwiftUI
中直接修改State
屬性值躯砰,就觸發(fā)視圖更新。
因為
State
是使用了@propertyDelegate
修飾的屬性值携丁,其內部實現(xiàn)應該是在狀態(tài)值set
方法中進行變更視圖的操作琢歇。
class Model: BindableObject {
var didChange = PassthroughSubject<Model, Never>()
var count: Int = 0 {
didSet {
didChange.send(self)// 調用didChange觸發(fā)變更操作
}
}
}
struct ContentView: View {
@State private var text: String = "a"http:// 使用@State修飾
@State private var model = Model()// 使用@State修飾
var body: some View {
VStack {
Text(text)
Text(model.count)
Button(action: {
self.text = "b"http:// 修改text會更新視圖
self.count += 1
}) {
Text("update-text")
}
}
}
}
- 如果想使用
class
類型屬性作為State
屬性,類對象需要實現(xiàn)BindableObject
協(xié)議梦鉴。當調用didChange
的send
方法時李茫,會通知關聯(lián)的View
更新視圖。didChange
是Publisher
(新出的Combine
異步事件處理框架肥橙,類似RxSwift
)類型魄宏,調用send
時會發(fā)送一個新的值給訂閱者。 - 當修改的
State
屬性值沒有在body
中使用或者修改后的State
屬性值和上一次相同存筏,并不會觸發(fā)重新計算body
宠互。
State
屬性修改時,會檢測State
屬性被使用和檢測值變更來決定要不要更新視圖和觸發(fā)body
方法椭坚。
-
State
屬性用class
類型予跌。在觸發(fā)body
重新計算前會檢查State
值有沒有改變,當修改類對象屬性時藕溅,因為類對象指針并沒有改變匕得,所以并不會觸發(fā)視圖更新。如果想觸發(fā)視圖變更巾表,可以在修改State
時生成新的對象(這種方式不太好)或者使用BindableObject
汁掠。
屬性
Property
與React
中的Props
類似,用于父視圖向子視圖傳遞值集币。
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
方法里也不能修改秽之,因為修改屬性會創(chuàng)建新的結構體。
@Binding
與Property
功能類似吃既,用于父視圖向子視圖傳遞值考榨。只不過Binding
屬性可以修改,修改Binding
屬性會觸發(fā)父視圖State
改變重新計算body
鹦倚『又剩可以實現(xiàn)反向數(shù)據(jù)流的功能,有點類似MVVM
的雙向綁定震叙。
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
相似掀鹅,暫時不太清楚使用上有什么區(qū)別。@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
注釋的地方任意使用結果都一樣扔嵌,視圖能正確更新。
@EnvironmentObject
通過Property
或者Binding
的方式猴鲫,我們只能顯式的通過組件樹逐層傳遞对人。
顯式逐層傳遞的缺點
- 當組件樹復雜的時候特別繁瑣,修改起來也很麻煩拂共。
- 有些屬性在視圖樹中間的層級不會使用到牺弄,只有底層會使用。會增加中間層級視圖的復雜度宜狐。也可以避免中間的層級重復計算
body
觸發(fā)視圖更新势告。
為了避免層層傳遞屬性,可以使用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
方法注入對象到組件樹中俭驮,子組件樹中共享同一個對象并且可以監(jiān)聽變更回溺。 -
@EnvironmentObject
查找如何能獲取到對應的對象,大概是根據(jù)屬性的類型進行查找混萝,所以多個屬性只要類型相同遗遵,就能取到同樣的對象。當組件樹有多個組件使用environmentObject
方法注入同類型的對象時逸嘀,獲取時會查找最近的父組件的對象车要。
目前好像沒有方式實現(xiàn)根據(jù)不同的
key
來注入多個對象并獲取。
數(shù)據(jù)流
父視圖 -> 子視圖向下傳遞
- 不需要修改使用
Property
- 需要修改使用
@Binding
父視圖 -> 子視圖跨層級向下傳遞
- Environment
全局狀態(tài)層管理
- 應該是結合
Combine
框架根據(jù)模塊功能領域分層進行管理崭倘。
視圖更新流程
- 修改
State
觸發(fā)視圖更新翼岁,檢測State
是否被使用以及值是否被改變类垫。 - 重新計算
body
生成新的視圖樹,會重新創(chuàng)建所有子視圖的View
結構體琅坡。 - 遍歷所有子視圖悉患,判斷
View
結構體與更新前是否一致。當不一致時脑蠕,觸發(fā)子視圖更新购撼,調用子視圖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
更新時谴仙,會重新創(chuàng)建ChildView
結構體。 -
ChildView
中的State
都會重新創(chuàng)建碾盐,Struct
和Model
初始化方法中晃跺,count
一直為0
,即使ContentView
里State
曾經(jīng)修改過毫玖。但是下一次修改State
值時掀虎,State
會使用之前的值做運算。
不太清楚這里是如何處理的付枫,
State
雖然重新初始化了一次烹玉,似乎還是使用的之前的State
。
- 例如當點擊Button時阐滩,會修改
ChildView
中model
,model2
中count
+=1二打,當前count
=1。- 當
ChildView
重新創(chuàng)建時掂榔,model
继效,model2
初始化方法中,count
=0装获。- 當下一次點擊Button修改
count
值時瑞信,count
會在1的基礎上+1,之后count
=2穴豫。
性能
- 當視圖發(fā)生變更時凡简,由于
body
會經(jīng)常重新計算,所以應該盡量避免在body
中進行重復和耗時計算精肃。 - 視圖變更時秤涩,視圖組件
View
結構體會重新創(chuàng)建,所以應該避免在init
方法中進行重復和耗時計算肋杖。(包括屬性的重新生成) - 根據(jù)上面
State
的特性溉仑,當State
屬性為結構體或類時,應避免在init
方法中訪問或修改屬性状植。因為當State修改過后浊竟,在init
方法中獲取到的值不是正確的怨喘,修改值也會生效。