数据流基础
我们之前介绍的都是一些非常简单的情况:它们都不涉及变化的状态。然而,几乎所有的应用都需要依赖于变化的数据。我们需要一种方法,使得当数据更新时及时、正确地更新我们的 UI。我们将介绍 UI 与数据绑定过程中涉及的一些最基本的常见数据流。
State¶
我们来看一个简单的例子。在我们的 View 中,需要记录用户点击的次数,并显示在屏幕上。你可能想要这样写:给 ContentView 加上一个变量 count,然后在 Button 中更改这个值:
1 2 3 4 5 6 7 8 9 10 | |
但这样是不能正常工作的。
首先,这个代码并不能通过编译:
1 | |
原因是,struct 是值类型,一般的函数不能够修改其变量的值。
而且,有一个更加深层的原因。我们改变了 count,并且希望 Text("\(count)") 能显示最新的值。但是,ContentView 并不知道当 count 更新时需要更新 UI。
修改的方法很简单,只需要在 var count: Int = 0 前加上 @State:
1 | |
这样,就能够通过编译,并且我们的界面会随着 count 的更新而更新了:

@State 是什么语法?为什么能够修改 count?
@State 涉及到一种叫 property wrapper 的语法。简单来说,property wrapper 有如下特性:
- 是一个包装后的类型,但对外的表现就像是被包装前的数据一样
- 当被包装的值被设定或被读取时,执行特定的代码
因为我们实际上是修改 @State 所包装的值而非 @State 变量本身,所以是可以修改 count 的值的。
为什么更新 count 之后 UI 会自动更新?
上面关于 property wrapper 的特性具体到 @State,即:
- 我们可以直接取其所储存的值(如上面的
Text("\(count)")) - 它在被设定时会触发 View 重新计算
body(如上面的count += 1)
于是,通过 @State,我们实现了 UI 与数据的绑定,只要 count 更新,整个 View 就会被重新计算。我们的 UI 在任何时刻都反映了数据最新的值,我们只需要修改数据,而无需手动地更新 UI。
React 中的类似方法
如果你对 React 有所了解,@State 相当于:
- 对于
Component:通过state获取值;调用setState更改值 - 对于纯函数组件:
useState的两个返回值,分别对应获取和更改
实际上,View 和 React 中的纯函数组件概念上是一样的。SwiftUI 通过 Swift 的 property wrapper 语法,将获取值、设定值以及更新 UI 简化为一个变量的读取和设定,而 React 因为语言的限制,设定值以及更新 UI 只能采用调用函数的方法。
Binding¶
@State 解决了在单个 View 中根据数据更新 View 的问题。但是,有时我们为了重用代码或保持代码的简洁,需要将一些 View 拆分出来,这就产生了一个问题:如何在父 View 和子 View 中共享一个可由子 View 修改的值?也就是说,数据如何从子视图流向父视图?
比如,我们希望把「点击一次使得值加 1」的按钮封装起来,以便重复使用,那么上面的 Button 不再在 ContentView 中定义,而是在另一个 View 中出现:
1 2 3 4 5 6 7 8 | |
如何在这两个 View 共享 count 使得 IncrementButton 可以修改呢?这就需要 @Binding。在 IncrementButton 中,添加一个成员 @Binding var value: Int,并在 Button 的 action 中将其值加 1:
1 2 3 4 5 6 7 | |
而在父 View 中,通过在 @State 变量名前加上一个 $ 来传递 Binding:
1 2 3 4 5 6 7 8 9 10 | |
在 IncrementButton 中修改 value,ContentView 中的 count 就会同步变化。换而言之,我们实现了父 View 和子 View 数据的绑定。
关于 $count
在变量名前加 $ 并不是一种语法,而是编译器自动给 ContentView 加上了一个变量名为 $count 的成员变量,它的类型为 Binding。
更多的原生 View
掌握了 @Binding 后,你可以使用更多的与数据相关的原生 View 了。比如,一个文本框:
1 | |
当用户输入文本时,TextField 中的文本会同步到 myText 中。
你可以在文档中探索更多的原生 View。
$count 的实质
在文档中,你可以看到 Binding 有这样一种构造函数:
1 | |
第一个参数 get 用于返回 Binding 变量读取的值,第二个参数 set 是当 Binding 变量改变时所执行的操作。
因此,你完全可以用这种方法取代 $count:
1 2 3 4 5 6 | |
当 IncrementButton 获取 value 时,调用 get 获取当前的值;当 IncrementButton 改变 value 时,调用 set 更新 count。由 @State 生成的 Binding 变量无非就是实现这样的功能。
UI 是状态的函数¶
通过上面的一些例子,你应该能总结出 SwiftUI 中 View 的一些特点。最明显的是:View 唯一地由数据确定。如果将 @State、@Binding 所代表的数据视作一种“状态”,那么我们可以写出这样一条式子:
1 | |
其中的 f,其实就是 View 中的 body。每次 state 更新时,View 通过 body 的定义计算出新状态下的 UI,并进行更新。所以,这也是为什么 body 应当是一个计算变量,而不是储存变量——它本质上是一个函数。
这种性质一个很自然的结果是:我们无法获得 View 的引用,也无法直接修改其 UI,所有对 UI 的修改都源自于数据的改变。
我们在下一节中进行更深入的讨论。