在 SwiftUI 中,preference
(偏好设置)机制是一种视图间通信方式,用于子视图向父视图传递数据,解决了 SwiftUI 中数据单向流动(通常是父到子)的限制。
它的核心思想是:子视图可以定义并设置一些 "偏好值",父视图通过特定方式收集这些值并做出响应。
核心组成
- PreferenceKey(偏好键)
定义数据传递的 "协议",指定数据类型和默认值,是父子视图通信的桥梁。
swift
// 定义一个偏好键,用于传递CGFloat类型的数据
struct MyPreferenceKey: PreferenceKey {
// 数据类型
typealias Value = CGFloat
// 默认值
static var defaultValue: CGFloat = 0
// 合并多个子视图的偏好值(如存在多个子视图时)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue()) // 示例:取最大值
}
}
- 子视图设置偏好值
通过 .preference(key:value:)
修饰符设置具体值。
swift
struct ChildView: View {
var body: some View {
Text("子视图")
.preference(key: MyPreferenceKey.self, value: 100) // 设置偏好值
}
}
- 父视图读取偏好值
通过 .onPreferenceChange(_:perform:)
监听偏好值变化并处理。
swift
struct ParentView: View {
@State private var maxValue: CGFloat = 0
var body: some View {
VStack {
ChildView()
Text("收到的值:\(maxValue)")
}
.onPreferenceChange(MyPreferenceKey.self) { value in
maxValue = value // 响应偏好值变化
}
}
}
典型应用场景
- 动态获取子视图尺寸
例如,父视图需要根据子视图的实际宽度调整布局。 - 收集多个子视图状态
如导航栏需要汇总多个子视图的选择状态。 - 跨层级传递数据
无需通过@Binding
或ObservableObject
逐层传递,直接跨层级通信。
注意事项
- 单向传递:只能从子视图向父视图传递,无法反向。
- 合并策略 :当多个子视图设置同一偏好键时,需在
reduce
方法中定义合并规则(如取最大值、累加等)。 - 性能考量:频繁更新偏好值可能影响性能,需合理使用。
通过 preference
机制,SwiftUI 视图间的通信更加灵活,尤其适合处理布局相关的动态数据传递。
补充:
关于上述自定义PreferenceKey的代码中reduce函数进行进一步说明:
swift
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue()) // 示例:取最大值
}
reduce
方法是 SwiftUI 中 PreferenceKey
协议的核心方法之一,它的作用是合并多个子视图产生的偏好值 。当多个子视图同时设置了同一个 PreferenceKey
的值时,系统会通过这个方法将这些分散的值合并成一个最终值,供父视图使用。以下是对方法进行解析:
value
: 一个输入输出参数 (inout
),表示当前已经合并的结果值。nextValue
: 一个闭包,调用后会返回下一个子视图设置的偏好值。
工作流程
当父视图监听某个 PreferenceKey
时:
- 系统会遍历所有设置了该偏好键的子视图,收集它们的值。
- 从
defaultValue
开始,逐个将子视图的值与当前结果通过reduce
方法合并。 - 最终得到一个统一的值,传递给父视图的
onPreferenceChange
回调。
场景:假设三个子视图分别设置了偏好值 50
、80
、60
:
- 初始时,
value
为defaultValue
(示例中是0
)。 - 第一次合并:
value = max(0, 50)
→value
变为50
。 - 第二次合并:
value = max(50, 80)
→value
变为80
。 - 第三次合并:
value = max(80, 60)
→value
保持80
。 - 最终父视图收到的值是
80
(三个子视图中的最大值)。
其他常见合并策略
根据业务需求,reduce
可以实现不同的合并逻辑:
- 累加 :
value += nextValue()
(适合计数场景)。 - 取最小值 :
value = min(value, nextValue())
。 - 拼接字符串 :
value += nextValue()
(如果Value
是String
类型)。 - 存储所有值 :如果
Value
是数组类型,value.append(contentsOf: nextValue())
。
reduce
方法的核心作用是定义多值合并规则 ,让父视图能从多个子视图的偏好值中得到一个统一的结果。它是 PreferenceKey
协议中处理 "多对一" 数据传递的关键机制。
示例:使用PreferenceKey来实现以下案例效果:当多个子视图存在不同的状态时,每次子视图的状态更新都能实时同步到父视图中,效果如下:

具体实现代码:
swift
import SwiftUI
// 1. 定义偏好键,用于收集所有子视图的选中状态
struct SelectionPreferenceKey: PreferenceKey {
// 使用字典存储所有子视图的选中状态,键为子视图ID,值为是否选中
static var defaultValue: [String: Bool] = [:]
// 合并多个子视图的偏好值
static func reduce(value: inout [String: Bool], nextValue: () -> [String: Bool]) {
// 将新的子视图状态合并到现有字典中
value.merge(nextValue(), uniquingKeysWith: { _, new in new })
}
}
// 2. 扩展View,方便设置选中状态偏好
extension View {
func selectionStatus(id: String, isSelected: Bool) -> some View {
preference(key: SelectionPreferenceKey.self, value: [id: isSelected])
}
}
// 3. 子视图组件 - 可选中的选项卡
struct SelectableItemView: View {
let id: String
let title: String
@State private var isSelected: Bool = false
var body: some View {
HStack {
Text(title)
Spacer()
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.foregroundColor(isSelected ? .accentColor : .secondary)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(8)
.shadow(radius: 1)
.onTapGesture {
isSelected.toggle()
}
// 将当前选中状态通过偏好机制传递
.selectionStatus(id: id, isSelected: isSelected)
}
}
// 4. 父视图 - 汇总并展示所有子视图的选中状态
struct SummaryView: View {
// 存储所有子视图的选中状态
@State private var allSelections: [String: Bool] = [:]
// 计算选中的数量
private var selectedCount: Int {
allSelections.values.filter { $0 }.count
}
// 计算总数量
private var totalCount: Int {
allSelections.count
}
var body: some View {
VStack(spacing: 20) {
// 显示汇总信息
VStack {
Text("选中状态汇总")
.font(.headline)
Text("已选中: \(selectedCount)/\(totalCount)")
.font(.subheadline)
.foregroundColor(.secondary)
}
// 列出所有选项的选中状态
VStack(alignment: .leading, spacing: 8) {
ForEach(allSelections.sorted(by: { $0.key < $1.key }), id: \.key) { id, isSelected in
HStack {
Text("选项 \(id.last ?? "?"):")
Text(isSelected ? "已选中" : "未选中")
.foregroundColor(isSelected ? .accentColor : .secondary)
}
}
}
Spacer()
// 子视图区域
VStack(spacing: 12) {
SelectableItemView(id: "item1", title: "选项 1")
SelectableItemView(id: "item2", title: "选项 2")
SelectableItemView(id: "item3", title: "选项 3")
SelectableItemView(id: "item4", title: "选项 4")
}
}
.padding()
.navigationTitle("选中状态汇总示例")
// 监听偏好值变化,更新汇总数据
.onPreferenceChange(SelectionPreferenceKey.self) { newSelections in
allSelections = newSelections
}
}
}
// 5. 主视图容器
struct ContentView: View {
var body: some View {
NavigationView {
SummaryView()
}
}
}
// 预览
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
这个案例展示了如何使用 PreferenceKey
汇总多个子视图的状态数据,主要实现了以下功能:
-
数据传递机制:
- 定义了
SelectionPreferenceKey
偏好键,使用字典[String: Bool]
存储多个子视图的选中状态 reduce
方法通过合并字典的方式,收集所有子视图的状态数据
- 定义了
-
子视图实现:
SelectableItemView
是可交互的子组件,包含一个选中状态isSelected
- 点击子视图时会切换选中状态,并通过
selectionStatus
方法将状态传递给父视图
-
父视图汇总:
- 父视图
SummaryView
通过onPreferenceChange
监听所有子视图的状态变化 - 实时计算并展示选中数量与总数量的比例
- 列出每个子视图的具体选中状态
- 父视图
-
核心逻辑:
- 每个子视图维护自己的选中状态
- 当子视图状态变化时,通过偏好机制自动通知父视图
- 父视图汇总所有状态并更新 UI 展示
这种实现方式的优势在于:
- 子视图与父视图解耦,子视图不需要知道父视图的存在
- 可以轻松扩展更多子视图,父视图会自动汇总新添加的子视图状态
- 符合 SwiftUI 单向数据流的设计理念