场景
使用 SwiftUI 布局界面,经常会遇到需要子视图信息来确定最终形态的情况。
以 SwiftUI 的 NavigationView 为例。下面的代码创建了一个 NavigationView,它具有单个 root view 和一个导航标题。
1
2
3
4
5
6
7
8
9
10
struct Test: View {
var body: some View {
NavigationView {
Text("Hello, World!")
.font(.title)
.navigationBarTitle("Root View")
.background(Color.gray)
}.debug()
}
}

可以看出,.font 这类修饰器改变的是各自 view 子树的环境。不过,.navigationBarTitle 却恰恰相反。Text 本身并不关心导航标题,不过它的父视图 NavigationView 对此关心。
分析
借助 debug extension,分析得到视图树实际类型如下。
1
2
3
4
5
6
7
8
NavigationView<
ModifiedContent<
ModifiedContent<
Text,TransactionalPreferenceTransformModifier<NavigationTitleKey>
>,
_BackgroundModifier<Color>
>
>
可以看出,苹果在设计 NavigationView 的时候使用 TransactionalPreferenceTransformModifier 来使得 NavigationView 获取子视图定义的 navigationBarTitle。

将实际类型绘制成图(有简化)后,可以看出,background 是 NavigationView 的子视图,而 preference 又是 background 的修饰器的子视图。preference 的“父”视图(图中黑色节点)都可以通过 NavigationTitleKey 读取到子视图设置的 navigationBarTitle。
Tips
下面以获取子视图大小为例,讲解如何利用 preference 获取子视图信息。
在使用 preference 时,需要先定义一个 PreferenceKey,这点类比 NavigationTitleKey 即可。下面的代码定义了一个 SizeKey。
1
2
3
4
5
6
7
struct SizeKey: PreferenceKey {
static let defaultValue: CGSize? = nil
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
value = value ?? nextValue()
}
}
利用 GeometryReader 的 background 接受 Text 尺寸作为它的建议尺寸。这里我们不想绘制任何东西,所以使用 Color.clear 视图。
1
2
3
4
Text("Hello, World!")
.background(GeometryReader { proxy in
Color.clear.preference(key: SizeKey.self, value: proxy.size)
})
现在视图树上任意的父视图节点就可以通过 onPreferenceChange 获取到 Text 的大小了。
下面是完整的代码,通过获取 Text 视图的大小,同步 Rectangle 的大小,当 Text 大小改变的时候,Rectangle 也会跟着一起改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import SwiftUI
struct Test: View {
@State private var size: CGSize? = nil
var body: some View {
VStack {
Text("Hello, World!")
.font(.title)
.background(GeometryReader { proxy in
Color.clear.preference(key: SizeKey.self, value: proxy.size)
})
.onPreferenceChange(SizeKey.self) { value in
self.size = value
}
Rectangle()
.foregroundColor(.blue)
.frame(width: size?.width, height: size?.height)
}
}
}
struct SizeKey: PreferenceKey {
static let defaultValue: CGSize? = nil
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
value = value ?? nextValue()
}
}
struct Test_Previews: PreviewProvider {
static var previews: some View {
Test()
}
}
小结
利用 PreferenceKey 和 onPreferenceChange 可以方便地获取到子视图的信息,如果不希望绘制任何东西,可以利用 Color.clear 视图“占位”。
SwiftUI - 分析视图树的实际类型