PreferenceKey 是什么?如何使用它向上传递数据?
PreferenceKey 是 SwiftUI 中用于将数据从 子视图(Child View) 向 父视图(Parent View) 或 祖先视图 传递的一种机制。
在 SwiftUI 的标准数据流中,数据通常是向下流动的(通过 Binding、Environment 或构造函数参数)。但是,当你需要子视图告诉父视图某些信息(例如子视图的实际大小、滚动位置、或者自定义的导航标题)时,就需要用到 PreferenceKey。
核心概念
要使用 PreferenceKey,你需要理解三个部分:
- Key (键): 一个遵守
PreferenceKey协议的结构体,定义了数据的默认值以及如何合并多个子视图传来的数据。 - Sender (发送者/子视图): 使用
.preference(key:value:)修饰符发送数据。 - Receiver (接收者/父视图): 使用
.onPreferenceChange(key:perform:)监听数据的变化。
如何使用:分步指南
我们将通过一个经典的例子来演示:父视图获取子视图的大小(Size)。
第一步:定义 PreferenceKey
你需要创建一个遵循 PreferenceKey 协议的结构体。
plaintext
import SwiftUI
struct SizePreferenceKey: PreferenceKey {
// 1. 定义默认值:如果没有视图发送数据,父视图收到的就是这个值
static var defaultValue: CGSize = .zero
// 2. 定义合并逻辑 (Reduce)
// 当有多个子视图同时发送这个 Key 时,SwiftUI 需要知道如何合并它们。
// value: 当前累积的值 (inout)
// nextValue: 下一个视图传来的新值
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
// 在这个例子中,我们简单地用新值覆盖旧值
// 如果你想收集所有子视图的高度和,可以写成: value.height += nextValue().height
value = nextValue()
}
}
第二步:子视图发送数据
在子视图中,我们通常结合 GeometryReader 来获取尺寸,然后通过 .preference 发送出去。
plaintext
struct ChildView: View {
var body: some View {
Text("我是子视图,我的大小不确定")
.padding()
.background(Color.blue.opacity(0.2))
// 使用 background + GeometryReader 是获取自身尺寸的常用技巧
.background(
GeometryReader { proxy in
Color.clear // 不可见的视图
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
}
}
第三步:父视图接收数据
在父视图中,使用 .onPreferenceChange 来读取数据。
plaintext
struct ParentView: View {
@State private var childSize: CGSize = .zero
var body: some View {
VStack {
Text("父视图检测到的子视图大小:")
Text("宽: \(childSize.width, specifier: "%.1f"), 高: \(childSize.height, specifier: "%.1f")")
.font(.title2)
.bold()
Divider()
ChildView()
}
// 监听 Key 的变化
.onPreferenceChange(SizePreferenceKey.self) { newSize in
print("收到新尺寸: \(newSize)")
self.childSize = newSize
}
}
}
reduce 函数的进阶用法
如果你的父视图包含多个子视图,并且你想收集它们所有的数据,reduce 函数就非常关键。
假设你想计算列表中所有子视图的高度总和:
plaintext
struct TotalHeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
// 累加所有子视图传来的值
value += nextValue()
}
}
// 使用场景
VStack {
ChildA().preference(key: TotalHeightKey.self, value: 20)
ChildB().preference(key: TotalHeightKey.self, value: 30)
}
.onPreferenceChange(TotalHeightKey.self) { totalHeight in
// totalHeight 将会是 50
}
常见注意事项
避免循环更新 (Infinite Loops):
PreferenceKey的更新通常发生在布局阶段。- 如果在
.onPreferenceChange中修改了@State,这会触发视图重新渲染(Relayout)。 - 如果重新渲染导致子视图的大小或数据发生变化,子视图会再次发送 Preference,父视图再次更新 State……从而导致死循环(App 卡死或崩溃)。
- 解决办法: 确保 State 的改变确实是必要的,或者数据是稳定的。
传递层级:
PreferenceKey可以穿透多层视图。即使是爷爷视图(Grandparent)也能收到孙子视图(Grandchild)发出的 Preference,只要中间的视图没有拦截或覆盖它。
Reduce 的执行顺序:
- SwiftUI 会按照视图树的顺序遍历。
reduce决定了同级或子级的数据如何汇总给父级。
- SwiftUI 会按照视图树的顺序遍历。
总结
- 是什么: 它是 SwiftUI 中子传父的通信通道。
- 怎么用:
- 定义
struct Key: PreferenceKey。 - 子视图用
.preference(key: Key.self, value: ...)发送。 - 父视图用
.onPreferenceChange(Key.self) { ... }接收。
- 定义
- 最常用场景: 获取子视图的动态尺寸(配合
GeometryReader)、自定义导航栏标题/按钮、收集列表项信息。