基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

PreferenceKey 是什么?如何使用它向上传递数据?

知识点图片

PreferenceKey 是 SwiftUI 中用于将数据从 子视图(Child View)父视图(Parent View)祖先视图 传递的一种机制。

在 SwiftUI 的标准数据流中,数据通常是向下流动的(通过 BindingEnvironment 或构造函数参数)。但是,当你需要子视图告诉父视图某些信息(例如子视图的实际大小、滚动位置、或者自定义的导航标题)时,就需要用到 PreferenceKey


核心概念

要使用 PreferenceKey,你需要理解三个部分:

  1. Key (键): 一个遵守 PreferenceKey 协议的结构体,定义了数据的默认值以及如何合并多个子视图传来的数据。
  2. Sender (发送者/子视图): 使用 .preference(key:value:) 修饰符发送数据。
  3. 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
}

常见注意事项

  1. 避免循环更新 (Infinite Loops):

    • PreferenceKey 的更新通常发生在布局阶段。
    • 如果在 .onPreferenceChange 中修改了 @State,这会触发视图重新渲染(Relayout)。
    • 如果重新渲染导致子视图的大小或数据发生变化,子视图会再次发送 Preference,父视图再次更新 State……从而导致死循环(App 卡死或崩溃)。
    • 解决办法: 确保 State 的改变确实是必要的,或者数据是稳定的。
  2. 传递层级:

    • PreferenceKey 可以穿透多层视图。即使是爷爷视图(Grandparent)也能收到孙子视图(Grandchild)发出的 Preference,只要中间的视图没有拦截或覆盖它。
  3. Reduce 的执行顺序:

    • SwiftUI 会按照视图树的顺序遍历。reduce 决定了同级或子级的数据如何汇总给父级。

总结

  • 是什么: 它是 SwiftUI 中子传父的通信通道。
  • 怎么用:
    1. 定义 struct Key: PreferenceKey
    2. 子视图用 .preference(key: Key.self, value: ...) 发送。
    3. 父视图用 .onPreferenceChange(Key.self) { ... } 接收。
  • 最常用场景: 获取子视图的动态尺寸(配合 GeometryReader)、自定义导航栏标题/按钮、收集列表项信息。
00:00
00:00