onPreferenceChange 是一种在 SwiftUI 中使用偏好键(PreferenceKey)进行视图间数据传递的机制,通常用于监听子视图的某些动态变化并在父视图中作出响应。
基础语法
.onPreferenceChange(PreferenceKeyType.self) { newValue in
// 响应偏好值的变化
}
PreferenceKeyType.self:表示要监听的偏好值的类型,必须是一个遵循 PreferenceKey 协议的类型。
newValue:回调中接收到的最新值。
作用范围:仅监听作用域内的子视图设置的偏好值变化。
完整使用流程
PreferenceKey协议为:
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}
1、定义一个 PreferenceKey
必须定义一个 PreferenceKey 类型,用来存储偏好值。
struct CustomPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0 // 默认值
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
// 合并多个值的逻辑(默认取最新值)
value = nextValue()
}
}
defaultValue:定义偏好值的初始值。
reduce:定义合并逻辑(如果有多个子视图设置值)。
2、在子视图设置偏好值
使用 preference(key:value:) 方法,将数据传递给父视图。
GeometryReader { geo in
Color.clear
.preference(key: CustomPreferenceKey.self, value: geo.frame(in: .global).minY)
}
作用:传递 geo.frame(in: .global).minY 的值给 CustomPreferenceKey。
3、在父视图监听偏好值
使用 onPreferenceChange 监听偏好值变化:
.onPreferenceChange(CustomPreferenceKey.self) { newValue in
print("New Value: \(newValue)")
// 在此响应偏好值变化
}
每次子视图的偏好值发生变化时,父视图会接收到最新值并触发回调。
代码示例
场景 1:监听滚动位置
在滚动视图中,监听每个子视图的相对位置。
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct ContentView: View {
@State private var scrollOffset: CGFloat = 0
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<10, id: \.self) { index in
Text("Item \(index)")
.frame(width: 300, height: 150)
.background(Color.orange)
.cornerRadius(10)
.padding()
.background(GeometryReader { geo in
Color.clear.preference(key: ScrollOffsetPreferenceKey.self, value: geo.frame(in: .global).minY)
})
}
}
}
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
scrollOffset = value
print("Scroll Offset: \(value)")
}
}
}
运行效果
子视图的 frame 相对于全局坐标系的位置变化时,父视图可以通过 onPreferenceChange 获取更新值。
可以根据滚动位置动态调整视图内容。
场景 2:动态调整视图背景
根据子视图的位置动态改变父视图背景颜色。
struct BackgroundColorPreferenceKey: PreferenceKey {
static var defaultValue: Color = .white
static func reduce(value: inout Color, nextValue: () -> Color) {
value = nextValue()
}
}
struct ContentView: View {
@State private var backgroundColor: Color = .white
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<5, id: \.self) { index in
Text("Item \(index)")
.frame(width: 300, height: 150)
.background(index % 2 == 0 ? Color.orange : Color.blue)
.cornerRadius(10)
.preference(key: BackgroundColorPreferenceKey.self, value: index % 2 == 0 ? Color.orange : Color.blue)
}
}
}
.onPreferenceChange(BackgroundColorPreferenceKey.self) { newColor in
backgroundColor = newColor
}
.background(backgroundColor)
}
}
运行效果
滚动视图时,当前子视图的背景颜色会被赋值到父视图背景。
注意事项
1、子视图到父视图
onPreferenceChange 只能从子视图向父视图传递值。父视图不能直接修改子视图的状态。
2、多个值的合并
使用 PreferenceKey 时,多个子视图的值会被合并,通过实现 reduce 方法定义合并逻辑。
3、性能考虑
如果有大量子视图传递数据,可能会引发性能问题,应尽量优化合并逻辑。
总结
onPreferenceChange 提供了一种灵活的方式,让父视图能够感知子视图的动态变化,在需要动态布局、监听滚动或几何变化时非常有用,是 SwiftUI 开发的重要工具之一。