上一节,我们找到了一种可以获取 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 四周画各种形式的边了。

应该说这种通过 PreferenceKeyAnchor<T> 获取数据的办法,在现如今这个 SwiftUI 版本里,着实有点儿别扭,以至于就连 Apple 自己,都没把文档写的太清楚。不过,当你了解了这个思路,SwiftUI 就为你敞开了一扇通过另外一个场景的大门,通过这个功能,我们能实现很多看似无法通过 SwiftUI 实现的效果。而这也正是泊学 App 在绘制 Tag 时,获取尺寸信息使用的方法。

下一节,我们就回到泊学 App 项目,来完成 Tag 的实现。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们