SwiftUI是iOS13新出的声明式UI框架,将会完全改变以前命令式操作UI的开发方式。此文章主要介绍SwiftUI中状态管理的方式。
可变状态
@State
与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。
属性
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方法里也不能修改,因为修改属性会创建新的结构体。
@Binding
与Property功能类似,用于父视图向子视图传递值。只不过Binding属性可以修改,修改Binding属性会触发父视图State改变重新计算body。可以实现反向数据流的功能,有点类似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" // Statevar body: some View {VStack {BindingView(text: $text)// State变量使用$获取BindingText(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: Modelvar 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触发视图更新。
为了避免层层传递属性,可以使用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// EnvironmentObjectvar body: some View {Text(model.text)}}struct ContentView: View {var body: some View {//EnvironmentObject需要使用environmentObject方法注入到组件树中EnvironmentView1().environmentObject(Model())}}
- 通过
environmentObject方法注入对象到组件树中,子组件树中共享同一个对象并且可以监听变更。 @EnvironmentObject查找如何能获取到对应的对象,大概是根据属性的类型进行查找,所以多个属性只要类型相同,就能取到同样的对象。当组件树有多个组件使用environmentObject方法注入同类型的对象时,获取时会查找最近的父组件的对象。
目前好像没有方式实现根据不同的
key来注入多个对象并获取。
数据流
父视图 -> 子视图向下传递
- 不需要修改使用
Property -
父视图 -> 子视图跨层级向下传递
-
全局状态层管理
-
视图更新流程
修改
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 = 0init() {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 = 0var body: some View {return VStack {Text("\(model.count)")Text("\(model2.count)")Text("\(count)")Button(action: {// 修改 Stateself.model.count += 1self.count += 1self.model2.update()}) {Text("update")}}}}struct ContentView: View {@State private var count = 0var 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方法中获取到的值不是正确的,修改值也会生效。
