Article / 2026/05/29
SwiftUI PreferenceKey 完整指南 🚀
[概述](概述) [PreferenceKey 协议](preferencekey协议) [核心 API](核心api) [使用场景](使用场景) [完整示例](完整示例) [高级用法](高级用法) [最佳实践](最佳实践)
SwiftUI PreferenceKey 完整指南 🚀
目录
概述
什么是 PreferenceKey?
PreferenceKey 是 SwiftUI 中用于子视图向父视图传递数据的机制。它实现了自下而上的数据流,与 @Binding、@State 等自上而下的数据流相反。
为什么需要 PreferenceKey?
在 SwiftUI 中,数据通常从父视图流向子视图。但有时我们需要:
- 子视图将信息(如尺寸、位置)传递给父视图
- 收集多个子视图的信息
- 在不破坏视图封装的情况下获取子视图的属性
PreferenceKey 协议
协议定义
protocol PreferenceKey {
associatedtype Value
// 默认值
static var defaultValue: Value { get }
// 合并多个值的规则
static func reduce(value: inout Value, nextValue: () -> Value)
}
必需实现
1. defaultValue
- 类型:静态属性
- 作用:当没有子视图设置此 preference 时的默认值
- 必须实现
static var defaultValue: CGSize = .zero
2. reduce(value:nextValue:)
- 类型:静态方法
- 作用:定义如何合并来自多个子视图的值
- 参数:
value: 当前累积的值(inout 参数)nextValue: 闭包,返回下一个要合并的值
- 必须实现
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue() // 简单替换
}
核心 API
1. .preference(key:value:)
设置 preference 值
func preference<K>(key: K.Type, value: K.Value) -> some View where K: PreferenceKey
- 在子视图中使用
- 将值向上传递给父视图
- 可以在视图树的任何层级设置
示例:
Text("Hello")
.background(
GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
}
)
2. .onPreferenceChange(_:perform:)
监听 preference 值变化
func onPreferenceChange<K>(
_ key: K.Type,
perform action: @escaping (K.Value) -> Void
) -> some View where K: PreferenceKey, K.Value: Equatable
- 在父视图中使用
- 当 preference 值改变时触发回调
- 要求
Value遵循Equatable协议
示例:
VStack {
childView
}
.onPreferenceChange(SizePreferenceKey.self) { size in
print("子视图尺寸: \(size)")
}
完整代码
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
typealias Value = CGSize
}
struct ContentView: View {
@State private var childSize: CGSize = .zero
var body: some View {
VStack {
Text("Hello Hello")
.background(
GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometry.size)
}
)
Color.blue.frame(width: childSize.width, height: childSize.height)
}.onPreferenceChange(SizePreferenceKey.self) { size in
childSize = size
}
}
}
3. .transformPreference(_:_:)
转换 preference 值
func transformPreference<K>(
_ key: K.Type,
_ callback: @escaping (inout K.Value) -> Void
) -> some View where K: PreferenceKey
- 在传递过程中修改 preference 值
- 可以在视图树的任何层级使用
- 用于复杂的数据转换
示例:
someView
.transformPreference(SizePreferenceKey.self) { size in
size.width *= 2 // 将宽度翻倍后再向上传递
}
4. .backgroundPreferenceValue(_:_:)
使用 preference 值创建背景视图
func backgroundPreferenceValue<Key, T>(
_ key: Key.Type,
@ViewBuilder _ transform: @escaping (Key.Value) -> T
) -> some View where Key: PreferenceKey, T: View
- 读取 preference 值并创建背景视图
- 背景视图会响应 preference 值的变化
示例:
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
typealias Value = CGSize
}
struct ContentView: View {
@State private var childSize: CGSize = .zero
var body: some View {
ZStack {
Text("Hello Hello")
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: SizePreferenceKey.self,
value: geometry.size
)
}
)
Color.blue.frame(width: childSize.width, height: childSize.height)
}.backgroundPreferenceValue(SizePreferenceKey.self) { bounds in
Rectangle()
.stroke(Color.blue)
.frame(width: bounds.width, height: bounds.height)
}
}
}
5. .overlayPreferenceValue(_:_:)
使用 preference 值创建覆盖视图
func overlayPreferenceValue<Key, T>(
_ key: Key.Type,
@ViewBuilder _ transform: @escaping (Key.Value) -> T
) -> some View where Key: PreferenceKey, T: View
- 类似
backgroundPreferenceValue,但创建的是覆盖层 - 用于在内容上方添加视图
示例:
content
.overlayPreferenceValue(PointPreferenceKey.self) { point in
Circle()
.fill(Color.red)
.frame(width: 10, height: 10)
.position(point)
}
使用场景
1. 获取子视图尺寸
// 定义 PreferenceKey
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("Hello, World!")
.padding()
.background(
GeometryReader { geometry in
Color.clear
.preference(key: SizePreferenceKey.self,
value: geometry.size)
}
)
}
.onPreferenceChange(SizePreferenceKey.self) { size in
childSize = size
}
}
}
2. 收集多个子视图的数据
// 定义 PreferenceKey(数组类型)
struct ViewBoundsPreferenceKey: PreferenceKey {
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
// 使用
struct MultiChildView: View {
@State private var childBounds: [CGRect] = []
var body: some View {
VStack {
ForEach(0..<5) { index in
Text("Item \(index)")
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ViewBoundsPreferenceKey.self,
value: [geometry.frame(in: .global)]
)
}
)
}
}
.onPreferenceChange(ViewBoundsPreferenceKey.self) { bounds in
childBounds = bounds
}
}
}
3. 锚点(Anchor)传递
// 定义锚点 PreferenceKey
struct AnchorPreferenceKey: PreferenceKey {
static var defaultValue: Anchor<CGRect>?
static func reduce(value: inout Anchor<CGRect>?,
nextValue: () -> Anchor<CGRect>?) {
value = value ?? nextValue()
}
}
// 使用
struct AnchorView: View {
var body: some View {
VStack {
Text("Target")
.anchorPreference(
key: AnchorPreferenceKey.self,
value: .bounds
) { $0 }
}
.overlayPreferenceValue(AnchorPreferenceKey.self) { anchor in
GeometryReader { geometry in
if let anchor = anchor {
let rect = geometry[anchor]
Rectangle()
.stroke(Color.red, lineWidth: 2)
.frame(width: rect.width, height: rect.height)
.position(x: rect.midX, y: rect.midY)
}
}
}
}
}
完整示例
示例 1:自适应标签云
// PreferenceKey 定义
struct TagSizePreferenceKey: PreferenceKey {
typealias Value = [Int: CGSize]
static var defaultValue: [Int: CGSize] = [:]
static func reduce(value: inout [Int: CGSize],
nextValue: () -> [Int: CGSize]) {
value.merge(nextValue()) { $1 }
}
}
// 标签视图
struct TagView: View {
let id: Int
let text: String
var body: some View {
Text(text)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color.blue.opacity(0.2))
.cornerRadius(16)
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: TagSizePreferenceKey.self,
value: [id: geometry.size]
)
}
)
}
}
// 标签云视图
struct TagCloudView: View {
let tags = ["Swift", "SwiftUI", "iOS", "Xcode", "Apple"]
@State private var tagSizes: [Int: CGSize] = [:]
var body: some View {
VStack {
flowLayout
}
.onPreferenceChange(TagSizePreferenceKey.self) { sizes in
tagSizes = sizes
}
}
var flowLayout: some View {
// 根据 tagSizes 实现流式布局
// ...
EmptyView()
}
}
示例 2:滚动偏移追踪
// PreferenceKey 定义
struct ScrollOffsetPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
// 滚动视图
struct ScrollTrackingView: View {
@State private var scrollOffset: CGFloat = 0
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(0..<50) { index in
Text("Item \(index)")
.frame(height: 50)
}
}
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ScrollOffsetPreferenceKey.self,
value: geometry.frame(in: .named("scroll")).minY
)
}
)
}
.coordinateSpace(name: "scroll")
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
scrollOffset = offset
}
.overlay(
Text("Offset: \(scrollOffset, specifier: "%.1f")")
.padding()
.background(Color.white.opacity(0.8))
.cornerRadius(8),
alignment: .top
)
}
}
示例 3:视图间连线
// PreferenceKey 定义
struct ViewPositionPreferenceKey: PreferenceKey {
typealias Value = [String: CGPoint]
static var defaultValue: [String: CGPoint] = [:]
static func reduce(value: inout [String: CGPoint],
nextValue: () -> [String: CGPoint]) {
value.merge(nextValue()) { $1 }
}
}
// 连线视图
struct ConnectedNodesView: View {
@State private var positions: [String: CGPoint] = [:]
var body: some View {
ZStack {
// 节点
VStack(spacing: 100) {
NodeView(id: "node1", label: "Node 1")
NodeView(id: "node2", label: "Node 2")
}
// 连线
if let pos1 = positions["node1"],
let pos2 = positions["node2"] {
Path { path in
path.move(to: pos1)
path.addLine(to: pos2)
}
.stroke(Color.blue, lineWidth: 2)
}
}
.onPreferenceChange(ViewPositionPreferenceKey.self) { positions in
self.positions = positions
}
}
}
struct NodeView: View {
let id: String
let label: String
var body: some View {
Text(label)
.padding()
.background(Color.green.opacity(0.3))
.cornerRadius(8)
.background(
GeometryReader { geometry in
Color.clear
.preference(
key: ViewPositionPreferenceKey.self,
value: [id: CGPoint(
x: geometry.frame(in: .global).midX,
y: geometry.frame(in: .global).midY
)]
)
}
)
}
}
高级用法
1. 泛型 PreferenceKey
struct GenericPreferenceKey<T>: PreferenceKey {
static var defaultValue: T? { nil }
static func reduce(value: inout T?, nextValue: () -> T?) {
value = value ?? nextValue()
}
}
2. 组合多个 PreferenceKey
struct CombinedPreferenceKey: PreferenceKey {
struct Value {
var size: CGSize = .zero
var position: CGPoint = .zero
}
static var defaultValue: Value = Value()
static func reduce(value: inout Value, nextValue: () -> Value) {
let next = nextValue()
value.size = next.size
value.position = next.position
}
}
3. 条件性 Preference
struct ConditionalPreferenceKey: PreferenceKey {
static var defaultValue: CGSize?
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
// 只保留大于某个阈值的尺寸
let next = nextValue()
if let next = next, next.width > 100 {
value = next
}
}
}
最佳实践
1. 命名规范
- 使用描述性名称:
SizePreferenceKey而不是MyKey - 遵循驼峰命名:
ViewBoundsPreferenceKey - 添加
PreferenceKey后缀以便识别
2. 性能优化
避免频繁更新:
// ❌ 不好:每帧都更新
.preference(key: SizeKey.self, value: continuouslyChangingValue)
// ✅ 好:只在必要时更新
.preference(key: SizeKey.self, value: stableValue)
使用 Equatable:
struct OptimizedPreferenceKey: PreferenceKey {
struct Value: Equatable {
var size: CGSize
var color: Color
}
static var defaultValue: Value = Value(size: .zero, color: .clear)
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
3. 调试技巧
添加日志:
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
let next = nextValue()
print("Reducing: \(value) -> \(next)")
value = next
}
可视化 Preference 流:
.onPreferenceChange(SizePreferenceKey.self) { size in
print("📏 Size changed: \(size)")
// 处理尺寸变化
}
4. 常见陷阱
陷阱 1:忘记实现 reduce
// ❌ 只取最后一个值
static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
value = nextValue()
}
// ✅ 正确合并所有值
static func reduce(value: inout [CGSize], nextValue: () -> [CGSize]) {
value.append(contentsOf: nextValue())
}
陷阱 2:过度使用 PreferenceKey
// ❌ 简单情况不需要 PreferenceKey
// 可以直接使用 @Binding 或回调
// ✅ 复杂的跨层级数据传递才用 PreferenceKey
总结
PreferenceKey 的优势
✅ 实现子视图到父视图的数据传递
✅ 不破坏视图的封装性
✅ 可以聚合多个子视图的数据
✅ 支持复杂的数据流场景
何时使用 PreferenceKey
- 需要获取子视图的布局信息(尺寸、位置)
- 收集多个子视图的数据
- 实现自定义布局容器
- 创建视图间的连接关系
何时不使用 PreferenceKey
- 简单的父子通信(用
@Binding) - 全局状态管理(用
@EnvironmentObject) - 单向数据流已足够的场景
参考资源
- Apple 官方文档 - PreferenceKey
- WWDC 2019 - Building Custom Views with SwiftUI
- SwiftUI Lab - The Art of Preferences
最后更新时间:2026-01-22
Giscus 未启用:请在
src/site.config.ts中配置 repoId 与 categoryId。