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 | var text = "World" |
这里text是一个静态的数据,如果我们会改动该数据,比如下面添加一个Button,然后修改text,会发现编译报错。
1 | var text = "World" |
Struct是不可变的值类型,并且我们不可以在计算属性中修改变量,这个时候需要使用@State来标记text。根据前文我们知道,在修改text的时候,State内部会进入到wrappedValue的set方法,在这个方法内部SwiftUI会根据状态的变动计算要修改的范围,重新渲染视图,从而更新与text有依赖的所有视图。
1 | @State var text = "World" |
当我们需要获取变量最新的状态(值)的时候,我们可以使用@State来标记变量。比如
下面的TextField会改变email的值,Text会显示email最新的值,使用@State我们就完成了控件与变量之间的绑定,但是在TextField中会修改email,并且它的初始化需要一个Binding<String>
类型的数据。从前文我们知道State其实是一个Property Wrappers
,它的projectedValue是一个Binding类型
的数据,因此在这里我们可以使用$email
获得到一个Binding
1 | @State private var email: String = "" |
@Binding
基于精简视图的目的,我们将Image和TextField抽离成一个独立的View:InputView
,在内部,同样是使用@State将控件和变量进行绑定。
1 | @State private var email: String = "" |
但是通过示例发现,父View并不能将email变量、Text控件与InputView中的text进行绑定,如果要实现父View与子View之间的数据绑定,我们需要使用@Binding
来标记子View中的text,同时使用InputView的时候需要传递$email
。
1 | // in FatherView |
通过使用State与Binding,我们将InputView以及其子视图(TextField)、父视图之间的email数据完成了双向绑定。每使用@State标记一个属性的时候,其实就创建了一个source of truth
,而子视图中的数据需要和父视图中的数据源保持一致,以确保拥有的是唯一source of truth
,就需要将两个数据进行绑定,让数据之间形成一个依赖,而不是拥有一个新的数据。而且也不需要提供依赖数据的初始值,因为我们需要从唯一source of truth中获取。
@ObservedObject
如果现在我们需要使用三个InputView来做一个注册功能,那么就需要有三个@State的变量来分别为InputView提供source of truth。
1 | @State private var account: String = "" |
三个变量已经算比较多的了,如果变量继续增多,我们还需要继续添加变量,这将不是一个明智的选择。我们应该使用一个类型抽象这个场景下的数据,比如一个SingUpData。结合前文提到的Publisher,这里我们让SingUpData遵守ObservableObject协议,这样我们就可以观察属性的变化。
1 | class SingUpData: ObservableObject { |
然后我们删除注册需要的三个变量,使用SingUpData来代替,并使用@ObservedObject进行标记。这样在SingUpData有变化的时候,视图都会收到更新,进而重新渲染界面;同样的,在视图中修改SingUpData中的数据(比如account),不仅能同步到SingUpData中,还可以更新界面中的显示(比如Text(“your accout:($singUpData.account)”))。
BindableObject 和ObjectBinding已经被删除,替换他们的是与binding没有歧义的ObservableObject、ObservedObject。
1 | @ObservedObject private var singUpData = SingUpData() |
为什么不使用@State而使用@ObservedObject
需要注意的是,使用@ObservedObject标记的必须是引用类型,这一点和State不同,@State只能用于标记值类型,比如String、Int、Bool、Struct、Enum。那你可能会说,将SingUpData换成Struct不就可以使用@State来标记它了么!比如下面这样修改,答案当然是可以的,这正好验证了@State只能标记值类型这个观点。所以使用哪一个来作为source of truth取决于你的数据是什么类型的。
1 | @State private var singUpData = SingUpData() |
另外,如果我们在注册界面中添加一个提交按钮,它的状态根据输入的内容来决定,使用disabled(:)
可以决定按钮是否可用。
1 | // SingUpData is Struct |
但是看着参数中一堆的逻辑,我们想着应该将他们放入SingUpData中自己来决定,很遗憾的是,使用一个计算属性来简化逻辑之后,我们预期的效果并没有达到,按钮的状态并不会根据输入发生变化。不仅仅是因为它只是一个计算属性,还在于这个计算属性在外部使用的时候并没有与我们的Data Flow产生依赖。
1 | struct SingUpData { |
@Binding
@ObservedObject
BindableObject 和ObjectBinding已经被删除了,替换他们的是与binding没有歧义的ObservableObject、ObservedObject,这些都是直接以依赖,用于形成子视图与父视图之间的依赖。
@EnvironmentObject
兄弟视图或者祖父视图与视图之间的依赖关系就需要使用环境变量。EnvironmentObject,
在讲解他之前,我们看一下这样的一个示例,在父视图上使用accentColor或者font,暗夜模式,然后其深层次的子视图中的Text就会响应这些设置。
@Environment
@StateObject
参考文章
- 参考文章列表