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 视图能够在不破坏声明式范式的情况下进行通信,是实现复杂布局和交互的重要工具。

相关推荐
benbenAItalk6 小时前
数字人口播视频的批量生产实践:素材规范、任务编排与质量验收
java·前端·音视频
2601_962885726 小时前
如何用 Python 扫描 A 股跳空缺口并统计缺口回补概率?
java·前端·python
西瓜太郎12347 小时前
Claude Code、Codex CLI、Gemini CLI 能否共用一枚 Key?先看协议选择矩阵
前端·api 网关·claude code·gemini cli·codex cli
陈随易9 小时前
在Finch用了62亿词元,我认为这是新一代Agent工具之神
前端·人工智能·后端
水域安全老周9 小时前
水趣钓鱼救生衣专利拆解:两级锁紧如何解决落水人衣分离
java·前端·网络
计算机魔术师9 小时前
Anthropic CEO突然喊踩刹车,OpenAI罕见力挺:AI这辆车不能只踩油门了
前端
wing9810 小时前
从codex转战workbuddy使用一周的感受
前端·人工智能·后端
EatFan10 小时前
Java接入支付宝 JSAPI 支付保姆教程(二):流程讲解与前后端代码讲解
前端·spring boot·后端·微信小程序·小程序·uni-app
梦想平凡10 小时前
百游棋牌源代码开发搭建教程(五):房间创建、座位分配与请求幂等实现
前端·javascript·数据库·源代码管理
IT_陈寒11 小时前
Redis的Set操作居然能把我的服务整挂了?
前端·人工智能·后端