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" // 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)")
}
}
}
上面
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// 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
都会重新创建,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
方法中获取到的值不是正确的,修改值也会生效。