上一节,我们找到了一种可以获取 SwiftUI 组件尺寸的方法。这一节,我们来看如何把这个信息尺寸保存出来供我们在绘制 UI 时使用。
实际上,SwiftUI 在渲染界面的时候,允许我们给每一个 UI 组件保存一些额外的数据。这些数据可以通过某种方式查询出来,并在绘制其它 UI 的时候使用。对于上一节用到的 GeometryProxy
,如果你去看看文档就会发现,它除了有 size
属性之外,还有一个下标操作符:
subscript<T>(Anchor<T>) -> T
这个下标操作符使用的索引,是一个 Anchor<T>
类型的对象。这个 Anchor<T>
又是什么呢?Apple 是这样说的:An opaque value derived from an anchor source and a particular view。
emm...即便你认得句子里的每一个单词,估计也不知道它究竟在说什么,既然是一个不透明的类型,应该怎么用呢?
在渲染 UI 时保存数据
为了回答这个问题,我们得先来了解如何在渲染 SwiftUI 组件时,保存额外的数据。在上一节新建的 Demo 里,我们创建一个 PreferenceKey.swift,在这里,添加下面的代码:
struct TagPreferenceData {
let bounds: Anchor<CGRect>
}
它表示我们期望对 SwiftUI 组件保存的数据。Anchor
可以理解成稍后获取数据的方式,而 CGRect
可以理解成是要保存的具体数据。
接下来,再定义一个实现了 PreferenceKey
的类型:
struct TagPreferenceKey: PreferenceKey {
typealias Value = [TagPreferenceData]
static var defaultValue: [TagPreferenceData] = []
static func reduce(
value: inout [TagPreferenceData],
nextValue: () -> [TagPreferenceData]) {
value.append(contentsOf: nextValue())
}
}
它可以理解为是给渲染 UI 时额外保存的数据起个名字,例如,我们给标签保存的数据,名字就是 TagPreferenceKey
,一会儿等我们保存数据的时候,就更清楚了。这个协议约束了三个东西:
Value
表示这个名字下面保存的数据类型,在我们的例子中,就是[TagPreferenceData]
,通常这个类型都是个数组,当然,也不是必须如此;defaultValue
是数据的默认值,我们设置成空数组;reduce
表示在渲染 UI 树的时候,遇到多个同名值时的处理策略(例如一个子视图里有多个标签),这里我们的处理就是逐个添加到数组里就好了;
看到这,你可能你会觉得有点晕,因为到现在你可能还不知道我在干嘛。别着急,这很正常。现在只要知道 TagPreferenceData
是我们要在创建标签时额外保存的尺寸数据,TagPreferenceKey
是我们给尺寸数据起的名字就好了。
定义好了这两个类型之后,关键的就来了,如何在渲染 Text
时保存它的尺寸信息呢?把之前的 Text
代码改成这样:
Text("Hello, World!")
.padding(20)
.anchorPreference(
key: TagPreferenceKey.self,
value: .bounds,
transform: { [TagPreferenceData(bounds: $0)] }
)
.anchorPreference
就是全部的秘密了,它有三个参数:
key
指定保存数据时使用的名字,因此我们传递了之前定义的TagPreferenceKey.self
;value
是要保存的数据类型,由于在TagPreferenceData
我们要保存的数据是一个CGRect
,因此这里传递.bounds
。实际上,这个 Anchor.Source 类型的参数,有很多预定义的值,大家可以在这里找到详细的列表,我们就不一一列出来了;transform
是一个函数,对于我们这个例子来说,它接受一个Anchor<CGRect>
参数,这个参数里就包含了我们要提取出来的尺寸信息。返回值则是TagPreferenceKey.Value
的类型,也就是[TagPreferenceData]
,理解了这个签名之后,transform
的定义也就不难理解了;
这样,我们期望保存的 Text
尺寸,就用一种暂时无法直接提取的方式,在 SwiftUI 渲染界面的时候,被保存了。那到底该怎么读出来呢?
之前,给 Text
创建背景的时候,我们使用的是 .background
,实际上,SwiftUI 还提供了另外一个叫做 backgroundPreferenceValue
的 modifier,相比较 .background
,它多了可以通过 PreferenceKey
查询数据的功能。于是,我们可以在 .anchorPreference
后面,串联上下面的代码:
Text("Hello, World!")
.padding(20)
.anchorPreference(
key: TagPreferenceKey.self,
value: .bounds,
transform: { [TagPreferenceData(bounds: $0)] })
.backgroundPreferenceValue(TagPreferenceKey.self) {
preferences in
return GeometryReader { proxy in
return self.createBackground(proxy, preferences)
}
}
.backgroundPreferenceValue
的第一个参数,是在渲染 UI 时额外保存的数据的名字,在我们的例子中,就是 TagPreferenceKey
。第二个参数,是一个 closure,这个 closure 的参数是 TagPreferenceKey.Value
,也就是 [TagPreferenceData]
。最后,返回一个 View
作为背景。
在这个 closure 里,当我们用 GeometryReader
的时候,它的 closure 参数 proxy
,作为一个 GeometryProxy
对象,还记得它有一个接受 Anchor<T>
类型的下标操作符么?于是在我们这个例子中,接下来的事情就变成了,preferences
里有 Anchor<CGRect>
包装的尺寸,proxy
提供了通过 Anchor<CGRect>
读取数据的下标操作符。把它们结合一下,就可以把 Anchor
中的数据取出来了。为了演示这个用法,我们定义了一个方法 createBackground
:
func createBackground(
_ geometry: GeometryProxy,
_ preferences: [TagPreferenceData]) -> some View {
let p = preferences.first
let bounds = p != nil ? geometry[p!.bounds] : .zero
print(bounds)
return Color.orange
}
在它的实现里,由于我们的例子中,只有一个 Text
对象,因此 preferences.first
就是给 Text
单独保存的尺寸信息。之后,再用 geometry[p!.bounds]
,就可以用 Anchor<CGRect>
作为下标,把尺寸读出来了。为了方便观察它,我们把它打印在了控制台上,最后,返回了 Color.orange
作为背景,这是和使用普通 .background
一样的。
What's next?
现在,重新执行下这个例子,看到的结果,应该和最开始是完全一样的。只是这次,我们在运行时得到了这个文本的实际大小,有了这个数值,就可以在 Text
四周画各种形式的边了。
应该说这种通过 PreferenceKey
和 Anchor<T>
获取数据的办法,在现如今这个 SwiftUI 版本里,着实有点儿别扭,以至于就连 Apple 自己,都没把文档写的太清楚。不过,当你了解了这个思路,SwiftUI 就为你敞开了一扇通过另外一个场景的大门,通过这个功能,我们能实现很多看似无法通过 SwiftUI 实现的效果。而这也正是泊学 App 在绘制 Tag
时,获取尺寸信息使用的方法。
下一节,我们就回到泊学 App 项目,来完成 Tag
的实现。