希望你还记得,在制作视频卡片的时候,我们提到过一个叫做 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 官方提供的一个容器类,当然,它也是一个 View
。GeometryReader
的 init
方法接受一个 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 在一个藏得很深的地方,给我们提供了办法。