SwiftUI 中的 PreferenceKey 协议

SwiftUI 中的 PreferenceKey 协议

PreferenceKey 是 SwiftUI 中用于在视图层次结构中传递数据的协议,它允许子视图向上传递信息给祖先视图,而不需要显式的数据绑定。

基本概念

PreferenceKey 定义了一种机制,使子视图能够向父视图传递数据,父视图可以通过 onPreferenceChange 修饰符来监听这些数据的变化。

协议要求

实现 PreferenceKey 需要满足两个要求:

swift 复制代码
public protocol PreferenceKey {
    associatedtype Value
    
    // 提供默认值
    static var defaultValue: Self.Value { get }
    
    // 合并多个视图传递的值
    static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}

创建自定义 PreferenceKey

swift 复制代码
struct MyCustomPreferenceKey: PreferenceKey {
    // 默认值
    static var defaultValue: Int = 0
    
    // 合并策略 - 这里取最大值
    static func reduce(value: inout Int, nextValue: () -> Int) {
        value = max(value, nextValue())
    }
}

基本使用流程

  1. 定义 PreferenceKey
  2. 子视图设置偏好值 (使用 .preference(key:value:)
  3. 父视图监听变化 (使用 .onPreferenceChange(_:perform:)

实际应用示例

1. 传递简单值

swift 复制代码
struct MessagePreferenceKey: PreferenceKey {
    static var defaultValue: String = ""
    
    static func reduce(value: inout String, nextValue: () -> String) {
        value = nextValue()
    }
}

struct ChildView: View {
    var body: some View {
        Text("Hello")
            .preference(key: MessagePreferenceKey.self, value: "来自子视图的消息")
    }
}

struct ParentView: View {
    @State private var message = ""
    
    var body: some View {
        VStack {
            Text("收到消息: \(message)")
            ChildView()
        }
        .onPreferenceChange(MessagePreferenceKey.self) { value in
            message = value
        }
    }
}

2. 收集视图尺寸

swift 复制代码
struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct ContentView: View {
    @State private var childSize: CGSize = .zero
    
    var body: some View {
        VStack {
            Text("子视图尺寸: \(childSize.width) x \(childSize.height)")
            
            Rectangle()
                .fill(Color.blue)
                .frame(width: 200, height: 100)
                .background(
                    GeometryReader { geometry in
                        Color.clear
                            .preference(key: SizePreferenceKey.self, value: geometry.size)
                    }
                )
        }
        .onPreferenceChange(SizePreferenceKey.self) { size in
            childSize = size
        }
    }
}

3. 收集多个视图的位置信息

swift 复制代码
struct ViewFramePreferenceKey: PreferenceKey {
    static var defaultValue: [CGRect] = []
    
    static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
        value.append(contentsOf: nextValue())
    }
}

struct ContentView: View {
    @State private var frames: [CGRect] = []
    
    var body: some View {
        VStack {
            ForEach(0..<5) { index in
                Text("Item \(index)")
                    .padding()
                    .background(
                        GeometryReader { geometry in
                            Color.clear
                                .preference(
                                    key: ViewFramePreferenceKey.self,
                                    value: [geometry.frame(in: .global)]
                                )
                        }
                    )
            }
        }
        .onPreferenceChange(ViewFramePreferenceKey.self) { newFrames in
            frames = newFrames
        }
    }
}

高级用法

1. 自定义坐标空间

swift 复制代码
struct ContentView: View {
    @State private var headerFrame: CGRect = .zero
    
    var body: some View {
        VStack {
            Text("Header")
                .padding()
                .background(
                    GeometryReader { geometry in
                        Color.clear
                            .preference(
                                key: HeaderFramePreferenceKey.self,
                                value: geometry.frame(in: .named("container"))
                            )
                    }
                )
            
            Text("Header frame: \(headerFrame.debugDescription)")
        }
        .coordinateSpace(name: "container")
        .onPreferenceChange(HeaderFramePreferenceKey.self) { frame in
            headerFrame = frame
        }
    }
}

struct HeaderFramePreferenceKey: PreferenceKey {
    static var defaultValue: CGRect = .zero
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}

2. 组合多个 PreferenceKey

swift 复制代码
struct CombinedPreferenceView: View {
    @State private var size: CGSize = .zero
    @State private color: Color = .black
    
    var body: some View {
        VStack {
            Text("Size: \(size.width)x\(size.height)")
            Text("Color: \(color.description)")
            
            ColorView()
        }
        .onPreferenceChange(SizePreferenceKey.self) { size = $0 }
        .onPreferenceChange(ColorPreferenceKey.self) { color = $0 }
    }
}

struct ColorView: View {
    var body: some View {
        Rectangle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .background(
                GeometryReader { geometry in
                    Color.clear
                        .preference(key: SizePreferenceKey.self, value: geometry.size)
                        .preference(key: ColorPreferenceKey.self, value: .blue)
                }
            )
    }
}

struct ColorPreferenceKey: PreferenceKey {
    static var defaultValue: Color = .clear
    static func reduce(value: inout Color, nextValue: () -> Color) {
        value = nextValue()
    }
}

注意事项

  1. 性能优化:偏好值变化会触发视图更新,应避免频繁更新

  2. reduce 方法实现:根据需求合理实现合并策略(取最大值、累加、替换等)

  3. 与 GeometryReader 配合:常用于获取视图的几何信息

  4. 调试技巧 :可以使用 .transformPreference 修改偏好值

PreferenceKey 提供了一种强大的机制,使 SwiftUI 视图能够在不破坏声明式范式的情况下进行通信,是实现复杂布局和交互的重要工具。

相关推荐
0end15 小时前
AI Agent 学习笔记(三):上下文工程(下)—— KV Cache、提示词设计与 Agent Skills
前端·aigc·ai编程
CAD老兵6 小时前
在浏览器里对比 DWG/DXF 图纸 —— @mlightcad/cad-diff-viewer
前端·javascript·github
JasonYin6 小时前
AI 定义的 H5移动端 开发规范,直接抄作业!
前端
诗章与猫6 小时前
Leaflet 渲染 100 万 marker 卡成 PPT?我用 WebGL 重写了渲染器
前端
用户921080262866 小时前
在 AI 代码生成项目里接入 Thought:别展示“玄学思维链”,只展示用户真正关心的工具调用
前端
艾醒(AiXing-w)6 小时前
LangChain 1.0 入门(二):LangChain 全模型标准化接入最佳实践(小白参数详解版)
前端·javascript·langchain
计算机魔术师7 小时前
我看了 Hugging Face 的 Daily Papers,发现这件事
前端
yu俞娥宝7 小时前
Codex官网前端可抄吗?技术拆解、风险边界与合规借鉴方案
前端
捧 花8 小时前
FastAPI 基础语法:从一个完整接口理解 Web API 的设计
前端·python·fastapi·middleware
Hilaku8 小时前
一行 CSS 新特性干掉 20 行 JavaScript ?
前端·javascript·程序员