希望你还记得,在制作视频卡片的时候,我们提到过一个叫做 Tag 的组件,当时,因为还缺少一些需要铺垫的知识,我们说稍后再来看它的实现。那么现在,是时候来把这块内容补上了。

实际上,Tag 也是一个 UI 容器,它只定义了标签的外观,例如,是否包含圆角、标签的颜色等等,但它没有定义标签里的内容。于是,这就牵扯出一个问题,为了画出标签的外观,我们需要知道里面内容的具体尺寸。例如,把视频卡片上的两个标签放大,是这样的:

其中,白色的边框代表的是内容自身的尺寸。我们需要知道它的宽度,才能决定两边圆角的起始位置;需要知道它的高度,才能决定圆的半径。左边的标签,我们画了半个圆,右边的标签,在左上和右下画了 1/4 个圆。但这些圆的计算方式,是一样的。

有了这个思路之后,我们先试着来实现 Tag,等遇到困难的时候,要解决的问题就更明确了。Tag 的定义,在 BoxueUIKit / Resuable / SwiftUI / HOC / Tag.swift 里,先来写我们熟悉的部分:

public struct Tag<Content: View>: View {
  let color: Color
  let tl: CGFloat
  let tr: CGFloat
  let bl: CGFloat
  let br: CGFloat
  let content: Content
  let edgeInsects: EdgeInsets


  public init(
    color: Color = .orange,
    tl: CGFloat = 0, tr: CGFloat = 0, bl: CGFloat = 0, br: CGFloat = 0,
    edgeInsects: EdgeInsets = EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10),
    @ViewBuilder _ content: () -> Content) {
    self.color = color
    self.tl = tl
    self.tr = tr
    self.bl = bl
    self.br = br
    self.content = content()
    self.edgeInsects = edgeInsects
  }
}

Tag 的定义和之前各种卡片的实现是类似的,它也用一个泛型参数替代其包含的内容。其中:

  • color 表示标签的颜色;
  • tl / tr / bl / br 分别表示标签左上 / 左下 / 右上 / 右下四个位置圆角的大小;
  • content 表示标签里显示的内容;
  • edgeInsects 表示在 content 四周填充的大小;

接下来,就是构建 body 了。还是先“跟着感觉走”:

public var body: some View {
  content
    .padding(edgeInsects)
}

然而跟着感觉似乎也就只能走到这了。这只能让我们创建一个方形的标签,如何把标签的四个角画出来呢?根据一开始说的,我们需要知道 content 的尺寸,才能在 content 四周画出表示圆角的路径。那如何能知道 content 的尺寸呢?答案可能让你有点儿意外,就是没有特别直接的办法可以办到。我们得利用 SwiftUI 绘制界面的机制,把这个尺寸信息“偷”出来。

为了理解这个“偷数据”的想法,我们先把 Tag 放放,新建一个最简单的 SwiftUI 应用,来观察这个过程:

struct ContentView: View {
  var body: some View {
    Text("Hello, World!")
      .padding(20)
      .background(Color.orange)
  }
}

想一下,为什么 .background 可以把 Text 的背景设置成橘色呢?它是设置了 Text 的什么属性么?如果你观察下这个界面的结构就会发现,并不是。Color.orange 甚至都不是一个单纯的颜色,而是一个 View。也就是说,所谓的 background,是在 Text 上贴了一层大小相同的橘色 View,让它看起来像是背景罢了。在 Xcode 里,可以观察到,它们的关系是这样的:

background 又是如何知道 Text 占据的区域究竟有多大的呢?我们可以模拟 Color.orange,自己创建一个 View

struct OrangeColor: View {
  var body: some View {
    return GeometryReader { proxy in
      return Rectangle()
        .fill(Color.blue)
        .frame(width: proxy.size.width, height: proxy.size.height)
    }
  }
}

其中,GeometryReader 是 SwiftUI 官方提供的一个容器类,当然,它也是一个 ViewGeometryReaderinit 方法接受一个 closure,这个 closure 有一个 GeometryProxy 类型的参数,通过它,我们可以在容器自身的坐标系里,得到容器自身的尺寸。这和 UIKit 中视图的 bounds 属性是类似的。有了这个尺寸,就可以基于它绘制图形了。

现在,我们用这个 OrangeColor 替换掉系统的 Color.orange

Text("Hello, World!")
  .padding(20)
  .background(OrangeColor())

当然,执行的结果应该是一样的。为了确定 OrangeColor 中的尺寸设置生效了,如果我们把 frame 的宽和高减半:

Rectangle()
  .fill(Color.orange)
  .frame(width: proxy.size.width / 2, height: proxy.size.height / 2)

就能看出对应的效果了:

What's next?

不过别高兴的太早,虽然我们手头有了一种能获取 UI 尺寸的方法。但当前版本 SwiftUI 的 GeometryReader closure 参数里,貌似不允许存在多条语句:

编译器会给出一些奇怪的错误,这使得我们无法在这里根据 proxy 参数去绘制图形。为此,我们不仅要依赖 GeometryReader 把尺寸信息“偷”出来,还要想个办法把它保存起来,并在合适的时候用它绘制图形。好在,SwiftUI 在一个藏得很深的地方,给我们提供了办法。

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

¥ 59

按月订阅

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

开始订阅

¥ 512

按年订阅

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

开始订阅

¥ 1280

泊学终身会员

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

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