12-8.【SwiftUI】SwiftUI 的 PreferenceKey 是干什么的?典型场景有哪些?

1 阅读3分钟

如果说 @Environment 是数据**“垂直向下”流动的机制(从父到子),那么 PreferenceKey 就是 SwiftUI 中让数据“垂直向上”**传递的唯一官方通道(从子到父)。

在声明式 UI 中,父视图通常无法直接访问子视图的私有属性(比如子视图的尺寸、位置或内部状态)。PreferenceKey 允许子视图“向上传递”一条信息,由其父辈或祖辈进行收集和汇总。


1. 工作原理:数据的“逆流而上”

PreferenceKey 的核心是 Reduce(规约) 算法。

  1. 子视图发射 (Anchor) :子视图通过 .preference(key:value:) 抛出一个值。
  2. 树状汇总 (Reduce) :当多个子视图抛出同一个 Key 的值时,SwiftUI 会调用你定义的 reduce 函数,将这些值合并(比如取最大值、存入数组或直接覆盖)。
  3. 父视图接收 (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:传递布局尺寸、内部标识、子级属性。