SwiftUI — onPreferenceChange

onPreferenceChange 是什么? #

SwiftUI 中,onPreferenceChange 是一个修饰符,用于监听由 PreferenceKey 定义的值的变化。它允许在 子视图树中定义一些值 并向父视图传递这些值,父视图可以通过 onPreferenceChange 捕获这些值的变化并做出相应的调整。

主要用途:
onPreferenceChange 常用于在子视图和父视图之间进行通信。当需要从子视图中获取某些动态信息(如尺寸、布局、位置)并将这些信息应用到父视图时,这是一个非常便捷的机制。


工作原理 #

  1. 创建一个 PreferenceKey
    • 用于定义共享数据的类型和合并逻辑(当有多个子视图更新同一个值时)。
  2. 子视图通过 PreferenceKey 设置值
    • 使用 anchorPreferencepreference 修饰符。
  3. 父视图监听数据变化
    • 父视图通过 onPreferenceChange 捕获子视图传递的 PreferenceKey 值的变化。

核心方法签名 #

func onPreferenceChange<K>(
    _ key: K.Type,
    perform action: @escaping (K.Value) -> Void
) -> some View where K : PreferenceKey

参数说明: #

  • key: 指定监听的 PreferenceKey 的类型。
  • action: 一个闭包,用于当值发生变化时所执行的操作(会接收最新的值作为参数)。

PreferenceKey 的定义 #

PreferenceKey 是 SwiftUI 中用于存储、共享和传递值的协议,其核心接口如下:

protocol PreferenceKey {
    associatedtype Value
    static var defaultValue: Self.Value { get }
    static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}

关键点 #

  • defaultValue: 定义 PreferenceKey 的初始值,如果子视图没有提供具体值,则返回默认值。
  • reduce: 用于合并多视图传递的值(例如多子视图可能会同时向父视图提供不同的数据)。

实用场景和示例 #

1. 获取子视图的尺寸 #

一个很常见的场景是,获取子视图的尺寸并将它向上传递给父视图。

import SwiftUI

// 定义一个 PreferenceKey,用来存储子视图的尺寸
struct ViewSizeKey: PreferenceKey {
    static var defaultValue: CGSize = .zero
    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct ChildView: View {
    var body: some View {
        Text("Hello, World!")
            .background(
                GeometryReader { geometry in
                    // 使用 Preference 把尺寸传递给父视图
                    Color.clear
                        .preference(key: ViewSizeKey.self, value: geometry.size)
                }
            )
    }
}

struct ParentView: View {
    @State private var childSize: CGSize = .zero

    var body: some View {
        VStack {
            Text("Child size: \(childSize.width) x \(childSize.height)")
            ChildView()
        }
        .onPreferenceChange(ViewSizeKey.self) { newSize in
            // 当子视图尺寸发生变化时,更新父视图的 state
            childSize = newSize
        }
    }
}

运行效果:

  • 父视图通过监听 ViewSizeKey,实时获取子视图的尺寸并显示在界面上。

2. 从子视图获取滚动位置 #

onPreferenceChange 也可以用来监测子视图的滚动位置,并在父视图中使用这些数据。

import SwiftUI

// 定义 PreferenceKey 来存储滚动偏移量
struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

struct ScrollableChildView: View {
    var body: some View {
        ScrollView {
            GeometryReader { geo in
                // 将滚动位置(y 偏移)传递给父视图
                Text("Scroll down to see more...")
                    .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .global).minY)
            }
            .frame(height: 50) // 限制 GeometryReader 的高度
            ForEach(1...50, id: \.self) { index in
                Text("Row \(index)")
            }
        }
    }
}

struct ParentScrollView: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        VStack {
            Text("Scroll Offset: \(scrollOffset)")
                .padding()
            ScrollableChildView()
        }
        .onPreferenceChange(ScrollOffsetKey.self) { newOffset in
            // 实时监听滚动偏移并更新
            scrollOffset = newOffset
        }
    }
}

运行效果:

  • 父视图通过 onPreferenceChange 实时捕获 ScrollView 中的滚动位置变化。

3. 动态调整父视图的行为 #

你可以利用 onPreferenceChange 根据子视图的变化动态调整父视图,比如调整布局或触发动画。

struct DynamicSizeExample: View {
    @State private var textSize: CGSize = .zero

    var body: some View {
        VStack {
            Text("The text below dynamically resizes:")
                .padding()
            Text("Resize Me!")
                .font(.system(size: textSize.width > 200 ? 20 : 40))
                .frame(maxWidth: .infinity)
                .background(GeometryReader { geometry in
                    Color.clear
                        .preference(key: ViewSizeKey.self, value: geometry.size)
                })
            Spacer()
        }
        .onPreferenceChange(ViewSizeKey.self) { newSize in
            textSize = newSize
        }
        .padding()
    }
}

运行效果:

  • 文本的大小随父视图的宽度实时调整。当宽度超过一定值时,字体动态变小。

onPreferenceChange 的适用场景 #

场景描述
动态调整布局父视图根据子视图的大小动态调整自身的布局或外观,如响应子视图宽度变化调整字体大小。
滚动条检测父视图监听滚动条的滚动位置,并显示位置或触发行为(如导航条变透明)。
子视图间交互子视图通过 PreferenceKey 将数据上报给父视图,父视图将这些数据提供给其他子视图。
高级 UI 定制用于较复杂的界面布局,比如将子视图的一些动态布局属性(如大小或位置信息)传递给其他视图进行操作。

与其他机制的比较 #

onPreferenceChange 是 SwiftUI 特有的机制,与 BindingEnvironment 等工具的作用范围和用途有所不同。

机制用途
onPreferenceChange从子视图向父视图传递动态值,每次值变化都会触发父视图的更新。
@Binding用于双向绑定值,父视图与子视图之间共享同一个状态值(仅适用于直接父子关系)。
@Environment从环境中读取全局属性,以支持跨越多个视图层次的共享。
GeometryReader提供子视图的布局和位置信息,但需要配合 PreferenceKey 才能实现向父视图传递动态信息的功能。

注意事项 #

  1. 性能问题:

    • 如果子视图频繁更新 PreferenceKey 的值,可能会导致父视图频繁刷新(如几何数据频繁更新)。
    • 尽量减少高频调用的复杂操作。
  2. 适用场景限制:

    • 单向传递: 通过 onPreferenceChange,数据只能从子视图传递到父视图,无法双向更新。
  3. 关键点定义:

    • 确保你定义的 PreferenceKey 中的合并逻辑(reduce)适配实际场景。

总结 #

  • onPreferenceChangeSwiftUI 提供的子视图向父视图传递信息的工具
  • 它结合 PreferenceKey 使用,可实现子视图与父视图之间的通信。
  • 常用于捕获子视图的动态数据(如尺寸、位置或滚动偏移量),并动态调整父视图的布局或外观。
  • 是构建高级布局和动态设计的必备工具之一。
本文共 1718 字,上次修改于 Jan 11, 2025