场景
使用 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
视图“占位”。