SwiftUI中的Data Flow(二)

我们在SwiftUI中修改数据,依赖这些数据的UI就会自动更新出来最新的值,这看起来很神奇,但是如果你看了前文之后,就多多少少会猜测到一些,我们统称这为Data Flow。在SwiftUI中,Data Flow是一个很重要很核心的概念,在我们实际的开发中,需要合理的管理自己的data flow,以确保我们拥有唯一的数据源(source of truth)。

在UIKit中,我们使用的是命令式的编程方式(imperative programming),比如前文中ModelY的属性发生变动,需要同步手动的更新我们的ModelYView。SwiftUI则采用一种声明式的编程方式(declarative programming),轻量化视图,让我们只关注会发生变化的数据,Data Flow会来完成数据与视图之间的依赖绑定,从而将最新的状态更新到视图上。在这种方式下,如果数据发生变化,SwiftUI根据数据重新构建视图,并以一种高效的diff算法来最小化的变动视图结构,这不在我们本文的讨论范围。

下面我们会从一个小例子开始,逐步的学习了解SwiftUI中构建一个合理的Data Flow所提供的一些工具。

@State

我们可以像往常一样,在某些控件中使用变量来展示数据。

1
2
3
4
5
var text = "World"

var body: some View {
Text("Hello \(text)")
}

这里text是一个静态的数据,如果我们会改动该数据,比如下面添加一个Button,然后修改text,会发现编译报错。

1
2
3
4
5
6
7
8
9
10
var text = "World"

var body: some View {

Text("Hello \(text)")

Button("Modif") {
self.text = "世界"// ERROR: Cannot assign to property: 'self' is immutable
}
}

Struct是不可变的值类型,并且我们不可以在计算属性中修改变量,这个时候需要使用@State来标记text。根据前文我们知道,在修改text的时候,State内部会进入到wrappedValue的set方法,在这个方法内部SwiftUI会根据状态的变动计算要修改的范围,重新渲染视图,从而更新与text有依赖的所有视图。

1
2
3
4
5
6
@State var text = "World"

//...
Button("Modif") {
self.text = "世界"
}

当我们需要获取变量最新的状态(值)的时候,我们可以使用@State来标记变量。比如

下面的TextField会改变email的值,Text会显示email最新的值,使用@State我们就完成了控件与变量之间的绑定,但是在TextField中会修改email,并且它的初始化需要一个Binding<String>类型的数据。从前文我们知道State其实是一个Property Wrappers,它的projectedValue是一个Binding类型的数据,因此在这里我们可以使用$email获得到一个Binding的变量,交给TextField进行初始化使用。

1
2
3
4
5
6
7
8
9
10
11
12
@State private var email: String = ""

var body: some View {
Text("Your email is: \(email)")

HStack(spacing: 8){

Image(systemName: "envelope")

TextField("input email", text: $email)
}
}

@Binding

基于精简视图的目的,我们将Image和TextField抽离成一个独立的View:InputView,在内部,同样是使用@State将控件和变量进行绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@State private var email: String = ""

var body: some View {
Text("Your email is: \(email)")

InputView(icon:"envelope", placehold: "input email", text: email)
}

//
struct InputView: View {

var icon: String
var placehold: String
@State var text: String

var body: some View {

HStack(spacing: 8){

Image(systemName: icon)

TextField(placehold, text: $text)
}
}
}

但是通过示例发现,父View并不能将email变量、Text控件与InputView中的text进行绑定,如果要实现父View与子View之间的数据绑定,我们需要使用@Binding来标记子View中的text,同时使用InputView的时候需要传递$email

1
2
3
4
5
6
7
8
9
10
11
12
// in FatherView

InputView(icon:"envelope", placehold: "input email", text: $email)

// in InputView

struct InputView: View {

//@State var text: String
@Binding var text: String
//...
}

通过使用State与Binding,我们将InputView以及其子视图(TextField)、父视图之间的email数据完成了双向绑定。每使用@State标记一个属性的时候,其实就创建了一个source of truth,而子视图中的数据需要和父视图中的数据源保持一致,以确保拥有的是唯一source of truth,就需要将两个数据进行绑定,让数据之间形成一个依赖,而不是拥有一个新的数据。而且也不需要提供依赖数据的初始值,因为我们需要从唯一source of truth中获取。

@ObservedObject

如果现在我们需要使用三个InputView来做一个注册功能,那么就需要有三个@State的变量来分别为InputView提供source of truth。

1
2
3
4
5
6
7
8
9
10
11
12
13
@State private var account: String = ""
@State private var password: String = ""
@State private var confirmPassword: String = ""

var doForm: some View {
VStack{
InputView(icon: "person", placehold: "account", text: $account)

InputView(icon: "lock.square.stack", placehold: "password", text: $password)

InputView(icon: "lock.square.stack", placehold: "confirm password", text: $confirmPassword)
}
}

三个变量已经算比较多的了,如果变量继续增多,我们还需要继续添加变量,这将不是一个明智的选择。我们应该使用一个类型抽象这个场景下的数据,比如一个SingUpData。结合前文提到的Publisher,这里我们让SingUpData遵守ObservableObject协议,这样我们就可以观察属性的变化。

1
2
3
4
5
class SingUpData: ObservableObject {
@Published var account: String = ""
@Published var password: String = ""
@Published var confirmPassword: String = ""
}

然后我们删除注册需要的三个变量,使用SingUpData来代替,并使用@ObservedObject进行标记。这样在SingUpData有变化的时候,视图都会收到更新,进而重新渲染界面;同样的,在视图中修改SingUpData中的数据(比如account),不仅能同步到SingUpData中,还可以更新界面中的显示(比如Text(“your accout:($singUpData.account)”))。

BindableObject 和ObjectBinding已经被删除,替换他们的是与binding没有歧义的ObservableObject、ObservedObject。

1
2
3
4
5
6
7
8
9
10
11
@ObservedObject private var singUpData = SingUpData()

var body: some View {
VStack{
InputView(icon: "person", placehold: "account", text: $singUpData.account)

InputView(icon: "lock.square.stack", placehold: "password", text: $singUpData.password)

InputView(icon: "lock.square.stack", placehold: "confirm password", text: $singUpData.confirmPassword)
}
}

为什么不使用@State而使用@ObservedObject

需要注意的是,使用@ObservedObject标记的必须是引用类型,这一点和State不同,@State只能用于标记值类型,比如String、Int、Bool、Struct、Enum。那你可能会说,将SingUpData换成Struct不就可以使用@State来标记它了么!比如下面这样修改,答案当然是可以的,这正好验证了@State只能标记值类型这个观点。所以使用哪一个来作为source of truth取决于你的数据是什么类型的。

1
2
3
4
5
6
7
@State private var singUpData = SingUpData()

struct SingUpData {
var account: String = ""
var password: String = ""
var confirmPassword: String = ""
}

另外,如果我们在注册界面中添加一个提交按钮,它的状态根据输入的内容来决定,使用disabled(:)可以决定按钮是否可用。

1
2
3
4
5
6
// SingUpData is Struct
//..
Button("Commit") {
// singup handle
}
.disabled($singupData.account.wrappedValue.count < 4 || $singupData.password.wrappedValue != $singupData.confirmPassword.wrappedValue || $singupData.password.wrappedValue.count == 0)

但是看着参数中一堆的逻辑,我们想着应该将他们放入SingUpData中自己来决定,很遗憾的是,使用一个计算属性来简化逻辑之后,我们预期的效果并没有达到,按钮的状态并不会根据输入发生变化。不仅仅是因为它只是一个计算属性,还在于这个计算属性在外部使用的时候并没有与我们的Data Flow产生依赖。

1
2
3
4
5
struct SingUpData {
var disable: Bool {
account.count < 4 || password != confirmPassword || password.count == 0
}
}

@Binding

@ObservedObject

BindableObject 和ObjectBinding已经被删除了,替换他们的是与binding没有歧义的ObservableObject、ObservedObject,这些都是直接以依赖,用于形成子视图与父视图之间的依赖。

@EnvironmentObject

兄弟视图或者祖父视图与视图之间的依赖关系就需要使用环境变量。EnvironmentObject,

在讲解他之前,我们看一下这样的一个示例,在父视图上使用accentColor或者font,暗夜模式,然后其深层次的子视图中的Text就会响应这些设置。

@Environment

@StateObject

参考文章

  • 参考文章列表