如果说 @Environment 是数据**“垂直向下”流动的机制(从父到子),那么 PreferenceKey 就是 SwiftUI 中让数据“垂直向上”**传递的唯一官方通道(从子到父)。
在声明式 UI 中,父视图通常无法直接访问子视图的私有属性(比如子视图的尺寸、位置或内部状态)。PreferenceKey 允许子视图“向上传递”一条信息,由其父辈或祖辈进行收集和汇总。
1. 工作原理:数据的“逆流而上”
PreferenceKey 的核心是 Reduce(规约) 算法。
- 子视图发射 (Anchor) :子视图通过
.preference(key:value:)抛出一个值。 - 树状汇总 (Reduce) :当多个子视图抛出同一个 Key 的值时,SwiftUI 会调用你定义的
reduce函数,将这些值合并(比如取最大值、存入数组或直接覆盖)。 - 父视图接收 (OnPreferenceChange) :父视图通过
.onPreferenceChange(_:perform:)监听合并后的最终结果,并据此更新自己的状态。
2. 典型应用场景
场景一:获取子视图的几何尺寸(GeometryReader 的搭档)
这是最常见的用法。如果你想让父视图的背景色根据子视图的高度动态调整,或者实现多个子视图之间的“等高”效果。
- 痛点:
GeometryReader只能让子视图知道自己的尺寸。 - 解法:子视图用
GeometryReader测量后,通过PreferenceKey把高度发给父视图。
场景二:自定义导航栏标题或组件
类似于系统原生的 .navigationTitle()。
- 原理:你可能在很深的子页面里定义了一个标题,父级的
NavigationView实际上就是通过 Preference 机制收集到这个字符串并显示在顶部的。
场景三:视差滚动与联动的 UI 效果
在 ScrollView 中,子视图可以实时将其相对于屏幕的偏移量(Offset)传给父视图。父视图接收到坐标后,可以驱动头图的缩放或导航栏的透明度变化。
3. 核心实现代码示例
要实现一个 PreferenceKey,你需要定义一个遵循协议的类型:
Swift
struct MyHeightKey: PreferenceKey {
// 默认值
static var defaultValue: CGFloat = 0
// 如何合并多个子视图的值
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue()) // 比如我们想要最大的那个高度
}
}
4. 使用时的“防御式”坑点
坑一:死循环(The Layout Loop)
这是 PreferenceKey 最危险的地方。
- 现象:视图不断闪烁或 App 直接卡死。
- 成因:子视图将高度传给父视图 -> 父视图根据高度更新了状态 -> 状态更新导致父视图重绘 -> 父视图重绘导致子视图位置改变 -> 子视图再次发送新高度。
- 对策:在
onPreferenceChange中更新状态时,务必检查新值是否真的发生了显著变化(比如abs(new - old) > 0.1)。
坑二:隐性依赖
PreferenceKey 的传递是隐式的。如果中间层级的某个视图拦截并修改了这个 Preference(比如使用了一个空的 reduce),上层视图可能永远拿不到正确的数据。
坑三:性能成本
由于 Preference 的汇总涉及到整个视图树的遍历和 reduce 计算,在极其复杂的长列表中高频发送 Preference(如滚动位置)可能会导致主线程掉帧。
- 优化:尽量减少
reduce函数中的逻辑复杂度。
总结:数据流动的平衡
- 向下用 Environment:传递配置、主题、权限。
- 向上用 Preference:传递布局尺寸、内部标识、子级属性。