核心设计思想
SwiftUI的View是渲染界面的模型,而不是真正的界面:仅仅包含界面结构、元素和各种属性的描述,并不包含界面像素、绘图缓冲区、绘图上下文等和界面渲染相关的内容。
View可以理解接受界面Data作为参数,输出View的函数:View = func(Data)
struct ContactUs: View {
var body: some View { // ... }
}
复制代码
View在SwiftUI都是值类型
- 所有组件都无法通过间接引用的方式进行修改。
- 所有影响到View的依赖关系都必须通过创建时注入到View对象里。
struct ContactUs: View {
init(contactUsViewModel: ContactUsViewModel) {
self.viewModel = contactUsViewModel
}
}
复制代码
既然View是Struct它又如何动态的描述一个View的呢?
View会产生用户交互、会产生新的数据、会引起View的变化,而Struct是一个静态的值类型,他是如何做到动态变化的呢?
SwiftUI中的Property wrapper
@State
struct ContentView {
@State var content: String
var body: some View {
TextField("Placeholder", $content)
}
}
复制代码
@State
是 ContentView
的一个可信数据源。只要被 @State
修饰,通过它就可以控制 ContentView
上显示的内容了。
@State
是一个 property wrapper,在定义 TextField
的时候,使用了 $content
的形式。因此,我们不妨直接到 State
的定义中,看看它的两个最重要的属性wrappedValue
和 projectedValue
究竟是什么。
struct State<Value> {
var projectedValue: Binding<Value>
var wrappedValue: Value
}
复制代码
Value
的类型是 String
, State
的类型是 State<String>
, wrappedValue
的类型是 Value
, projectedValue
的类型是 Binding<String>
。
TextField
的 init
方法:
init<S>(
_ title: S,
text: Binding<String>,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}) where S : StringProtocol
复制代码
它的 text
参数也接受一个 Binding<String>
对象,这也就是为什么我们可以用 TextField("Placeholder", $content)
的形式创建 TextField
了。那么,这个 Binding
又是什么呢?
@Binding
在 Apple 的文档中,对 Binding
的描述是这样的:
Use a binding to create a two-way connection between a view and its underlying model.
$content
在 ContentView
代表的界面和 content
代表的数据之间,创建了双向绑定。
这是怎么实现的呢?
init(get: () -> Value, set: (Value) -> Void)
复制代码
Binding
本质就是一个 getter 和 setter,那么getter 究竟是从哪读数据?setter 又设置了什么呢?我们结合下面这张图解释这个问题:
ContentView.content
作为一个 property wrapper,它的 projectValue
是一个 Binding<String>
对象,这个 Binding<String>
的 getter 和 setter 对应着 wrappedValue
的 get
和 set
方法。
当用户在 ContentView
内置的 TextField
中输入内容时候,TextField
就可以通过注入的 Binding
对象间接修改 content
属性的值了。
由于 content
又是一个 @State
对象,SwiftUI 运行时就会在它的渲染队列中加入所有依赖这个属性的界面的渲染任务。当屏幕刷新的时候,所有受影响的部分就更新过来了。言外之意,属性变化到界面变化的过程,是一个串行的过程,只不过这个过程发生的很快(大部分 iOS 设备上都是每秒 60 次,iPad pro 则可以达到每秒 120 次),它并不会让我们感觉到有任何延迟。
总结
@State
非常适合 struct
或者 enum
这样的值类型,它可以自动为我们完成从状态 到 UI 更新等一系列操作。但是它本身也有一些限制,我们在使用 @State
之前,对 于需要传递的状态,最好关心和审视下面这两个问题:
-
这个状态是属于单个 View 及其子层级,还是需要在平行的部件之间传递和使用?
@State
可以依靠 SwiftUI 框架完成 View 的自动订阅和刷新,但这是有条件的:对于@State
修饰的属性的访问,只能发生在body
或者body
所调 用的方法中。你不能在外部改变@State
的值,它的所有相关操作和状态改变都应该是和当前 View 挂钩的。如果你需要在多个 View 中共享数据,@State
可能不是很好的选择;如果还需要在 View 外部操作数据,那么@State
甚至就不是可选项了。 -
状态对应的数据结构是否足够简单?对于像是单个的 Bool 或者 String,
@State
可以迅速对应。含有少数几个成员变量的值类型,也许使用@State
也还不错。但是对于更复杂的情况,例如含有很多属性和方法的类型,可能其中只有很少几个属性需要触发 UI 更新,也可能各个属性之间彼此有关联,那么我们应该选择引用类型和更灵活的可自定义方式。
对于这样的不适合选择 @State
的情况 (往往这是实际数据传递中 更普遍的情况),ObservableObject
和 @ObservedObject
是解决的方案。
思考
@State
属性值和普通属性在View模型表现。