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())
}
}
基本使用流程
- 定义 PreferenceKey
- 子视图设置偏好值 (使用
.preference(key:value:)
) - 父视图监听变化 (使用
.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()
}
}
注意事项
-
性能优化:偏好值变化会触发视图更新,应避免频繁更新
-
reduce 方法实现:根据需求合理实现合并策略(取最大值、累加、替换等)
-
与 GeometryReader 配合:常用于获取视图的几何信息
-
调试技巧 :可以使用
.transformPreference
修改偏好值
PreferenceKey 提供了一种强大的机制,使 SwiftUI 视图能够在不破坏声明式范式的情况下进行通信,是实现复杂布局和交互的重要工具。