SwiftUI中的界面是严格数据驱动的:运行时界面的修改,只能通过修改数据来间接完成,而不是直接对界面进行修改操作。
数据处理的基本原则
-
Data Access as a Dependency
:在 SwiftUI 中数据一旦被使用就会成为视图的依赖,也就是说当数据发生变化了,视图展示也会跟随变化,不会像 MVC 模式下那样要不停的同步数据和视图之间的状态变化。 -
A Single Source Of Truth
: 保持单一数据源,在 SwiftUI 中不同视图之间如果要访问同样的数据,不需要各自持有数据,直接共用一个数据源即可,这样做的好处是无需手动处理视图和数据的同步,当数据源发生变化时会自动更新与该数据有依赖关系的视图。
五个数据流工具
可以通过它们建立数据和视图的依赖关系
Property
@State
@Binding
ObservableObject
@EnvironmentObject
注意:后面四种使用 Swift 5.1 的新特性 Property Wrapper
来实现的一种属性装饰语法糖(修饰器/装饰器)
Property
- 这种形式最简单,就是在
View
中定义常量或者变量,然后在内部使用
import SwiftUI
struct Model {
var title: String
var info: String
}
struct ContentView : View {
let model = Model(title: "WWDC 2019", info: "SwiftUI是一个全新的UI框架")
var body: some View {
VStack {
// 内部使用前面定义的let和var
Text(model.title).font(.title)
Text(model.info)
}
}
}
@State
- 前面已经使用过多次,作用是让被它标记的属性可以在
View
内部进行修改,因为直接修改会报错。 - 用
@State
修饰的属性,只要属性改变,SwiftUI 内部会自动的重新计算 View的body
部分,构建出View Tree
,由于 View 都是结构体,SwiftUI 每次构建这个View Tree
都极快,这使得性能有很强的保障。 - 开发者不需要关心数据和视图的状态同步工作,只需要关心数据的获取以及逻辑处理,使用起来非常简单,大大提高了开发效率。
- 使用的时候,属性前添加
$
符号,这种属性称之为projection property
(投影属性)。 - 只能在当前 View 的
body
内修改,所以它的使用场景是只影响当前 View 内部的变化的操作。 - 通常应该被标记
private
。
@Binding
- 传统的 GUI 程序中最复杂的部分莫过于状态管理,尤其是多数据同步,一个数据存在于不同的 UI 中,针对某个数据导致的 UI 变化理论上应该同步,状态量的变多加上异步的操作,会使程序的可读性直线下降,并且伴随着而来的就是各种 Bug,SwiftUI 的解决办法就是使用
@Binding
。 - 系统提供的 Control(可操作的View) 的构造器基本都需要
@Binding
属性,可以自动的同步来自 API 调用方的数据。 -
@Binding
主要有两个作用:- 在不持有数据源的情况下,任意读取。
- 从
@State
中获取数据应用,并保持同步。
struct ContentView: View {
// 用@State修饰需要改变的变量
@State private var count: Int = 0
var body: some View {
VStack {
Text("\(count)").foregroundColor(.orange).font(.largeTitle).padding()
// $访问传递给另外一个UI
CountButton(count: $count)
}
}
}
struct CountButton : View {
// 用@State修饰,绑定count的值
@Binding var count: Int
var body: some View {
Button(action: {
// 此处修改数据会同步到上面的UI
self.count = self.count + 1
}) { Text("改变Count")
}
}
}
@State VS @Binding
@State
只能在当前修饰的属性改变时会触发UI刷新,所以很适合值类型,因为对值类型里面属性的更新,也会触发整个值类型的重新设置。不过值类型在传递时会发生复制操作,所以给传递后的值类型即使属性更新了也不会触发最初的传过来的值类型的重新赋值,所以界面并不会刷新,此时需要用@Binding
,因为它可以将值类型转为引用类型,这样在传递时,其实是一个引用,任何一方修改属性都会触发值类型的重新设置,UI界面也随之更新。
ObservableObject
- 在应用开发过程中,很多数据其实并不是在
View
内部产生的,这些数据有可能是一些本地存储的数据,也有可能是网络请求的数据,这些数据默认是与 SwiftUI 没有依赖关系的,要想建立依赖关系就要用 ObservableObject,与之配合的是@ObservedObject
和@Published
。 -
@Published
是 Xcode11 beta5 之后新增的代理属性,此属性如果用在 ObservableObject 内,一旦修饰的属性发送了变化,会自动触发 ObservableObject 的objectWillChange
的send
方法,刷新页面,SwiftUI 已经默认帮我实现好了,但也可以自己手动触发这个行为。 - ObservableObject 是一个协议,必须要类去实现该协议。
- ObservableObject 适用于多个 UI 之间的同步数据。
- 基本使用
class User: ObservableObject {
@Published var name = "" // @Published修饰需要监听的属性,一旦变化就会发出通知,它是发布者
@Published var address = ""
}
struct ContentView: View {
@ObservedObject var User = User() // @ObservedObject修饰ObservableObject
var body: some View {
}
}
- 案例
class UserSettings: ObservableObject {
// 有可能会有多个视图使用,所以属性未声明为私有
@Published var score = 123
}
struct ContentView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
Text("人气值: \(settings.score)").font(.title).padding()
Button(action: {
self.settings.score += 1
}) {
Text("增加人气")
}
}
}
}
- 手动发送状态更新
class UserSettings: ObservableObject {
// 1.添加发布者,实现一个属性,名字不能乱写,否则没有效果
let objectWillChange = ObservableObjectPublisher()
// 2.只要name发生更改,属性观察器就会调用,告诉objectWillChange发布者发布有关我们的数据已更改的消息,以便所有订阅的视图都可以刷新的消息
var name = "" {
willSet {
// 3.使用发布者
objectWillChange.send()
}
}
}
struct ContentView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
TextField("姓名", text: $settings.name)
.textFieldStyle(RoundedBorderTextFieldStyle()).padding()
Text("你的姓名: \(settings.name)")
}
}
}
@EnvironmentObject
- 主要是为了解决跨组件(跨应用)数据传递的问题。
- 组件层级嵌套太深,就会出现数据逐层传递的问题,
@EnvironmentObject
可以帮助组件快速访问全局数据,避免不必要的组件数据传递问题。 - 使用基本与
@ObservedObject
一样,但@EnvironmentObject
突出强调此数据将由某个外部实体提供,所以不需要在具体使用的地方初始化,而是由外部统一提供。 - 使用
@EnvironmentObject
,SwiftUI 将立即在环境中搜索正确类型的对象。如果找不到这样的对象,则应用程序将立即崩溃。
// 和@ObservableObject一样
class User: ObservableObject {
@Published var name = ""
@Published var address = ""
}
struct ContentView: View {
@EnvironmentObject var User // 注意这里不需要初始化
var body: some View {
}
}
- 案例
class UserSettings: ObservableObject {
@Published var score = 123
}
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
NavigationView{
VStack {
// 显示score
Text("人气值: \(settings.score)").font(.title).padding()
// 改变score
Button(action: {
self.settings.score += 1
}) {
Text("增加人气")
}
// 跳转下一个界面
NavigationLink(destination: DetailView()) {
Text("下一个界面")
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack {
Text("人气值: \(settings.score)").font(.title).padding()
Button(action: {
self.settings.score += 1
}) {
Text("增加人气")
}
}
}
}
// 需要注意此时需要修改SceneDelegate,传入environmentObject
window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
总结
SwiftUI中视图不再是一系列操作事件而是数据的函数式表现。通过这种编程思想的改变,SwiftUI 帮助你管理各种复杂的界面和数据的处理,开发者只需要关注数据的业务逻辑即可,但是要想管理好业务数据,还得要遵循数据的流转规范才可以,官方为我们提供了一个数据流图。
从上图可以看出SwiftUI 的数据流转过程:
- 用户对界面进行操作,产生一个操作行为
action
- 该行为触发数据状态的改变
- 数据状态的变化会触发视图重绘
- SwiftUI 内部按需更新视图,最终再次呈现给用户,等待下次界面操作
注意
- 在 SwiftUI 中,开发者只需要构建一个视图可依赖的数据源,保持数据的单向有序流转即可,其他数据和视图的状态同步问题 SwiftUI 帮你管理,所以
ViewController
在这里也就不需要了,再也不存在臃肿瘦身的问题了。 - SwiftUI 的界面不再像 UIKit 那样,用
ViewController
承载各种UIVew
控件,而是一切皆View
,所以可以把View
切分成各种细粒度的组件,然后通过组合的方式拼装成最终的界面,这种视图的拼装方式大大提高了界面开发的灵活性和复用性,视图组件化并任意组合的方式是 SwiftUI 官方非常鼓励的做法。 -
Property、 @State、 @Binding
一般修饰的都是View
内部的数据。 -
@ObservedObject、 @EnvironmentObject
一般修饰的都是View
外部的数据:- 系统级的消息
- 网络或本地存储的数据
- 界面之间互相传递的数据