在 SwiftUI 中,组件间的数据传递通常依赖于如 @State、@Binding、@Environment 等机制。但如果希望将子视图中的某些状态或信息传递给父视图,该如何处理呢?答案就是:使用Preference
。
引入
navigationBarTitle
修饰符如何将数据传递给父视图 NavigationView?其实,它用的就是 Preference 机制。
swift
import SwiftUI
// 模拟实现
// 1.遵守协议
struct NavigationBarTitleKey: PreferenceKey {
static var defaultValue: String = ""
static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}
struct ContentView: View {
var title: String = "标题"
var body: some View {
NavigationView {
Text("SwiftUI")
.navigationBarTitle(title)
// 2.使用preference修饰符将(NavigationBarTitleKey,title)传出去
.preference(key: NavigationBarTitleKey.self, value: title)
}
// 3.使用onPreferenceChange修饰符来观察NavigationBarTitleKey
.onPreferenceChange(NavigationBarTitleKey.self) { title in
// 打印Title
print(title)
}
}
}
Preference
与Environment
功能类似,都可以跨视图传递数据。不同的是Preference
的应用场景是将数据从子视图传递到父视图。
swift
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView { // 父View
Text("SwiftUI") // 子View
.navigationBarTitle("标题")
}
}
}
PreferenceKey协议
- SwiftUI 并没有提供 @Preference 这样简单的属性包装器,想要使用 Preference,必须定义一个结构体遵守 PreferenceKey 协议。
- 作用:当一个 View 有多个子 View 时,会将子 View 设定的值合并为父 View 可见的布局条件。
- 声明如下。
swift
public protocol PreferenceKey {
associatedtype Value
static var defaultValue: Self.Value { get }
static func reduce(value: inout Self.Value, nextValue: () -> Self.Value)
}
参数说明
Value
:是一种类型别名,放置需要传递的数据的类型。defaultValue
:未自定义值时,将使用此 defaultValue。reduce
:这是一个静态函数,用它来合并(累加,替换)视图层次中查询到的所有 Preference 值。- 子 View 使用
preference
修饰符对 Preference 进行设置,简单理解就是一个(Key,Value)
对,只不过这个Key
需要遵守 PreferenceKey 协议,Value
就是想捕获的内容。 - 父 View 使用
onPreferenceChange
修饰符监听 Preference 值的改变,并能通过Key
捕获到其对应的Value
。
使用步骤
- 定义 Preference 的类型,即 Value 的类型。
- 定义 defaultValue,如果在 View 的层次结构中从未设置过值,则使用该值。
- 实现 reduce 函数,合并在视图层次中不同级别设置的 Preference 值。
案例
swift
import SwiftUI
struct SizePreferenceKey: PreferenceKey {
typealias Value = CGSize
static let defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
// 计算某个View的Size
struct ContentView: View {
@State private var textSize: CGSize = .zero
var body: some View {
VStack {
Text("SwiftUI 实用教程")
.background(
GeometryReader { proxy in
Color
.clear
// 通过preference将(Key,Value)传出去
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
Text("\(String(describing: textSize))")
}
// 通过onPreferenceChange监听preference的变化,通过Key拿到Value
.onPreferenceChange(SizePreferenceKey.self) { size in
self.textSize = size
}
}
}
应用
Preference 是一个非常强大的工具,但它们主要用于通用视图组件(如NavigationView),这些组件可以包含非常复杂的视图层次结构。
- 定义 PresentableAlert,Equatable 协议必须要实现,本案例还需要实现 Identifiable 协议。
swift
struct PresentableAlert: Equatable, Identifiable {
let id = UUID()
let title: String
let message: String
}
- 自定义 PreferenceKey。
swift
struct AlertPreferenceKey: PreferenceKey {
static var defaultValue: PresentableAlert?
static func reduce(value: inout PresentableAlert?, nextValue: () -> PresentableAlert?) {
value = nextValue()
}
}
- 使用
preference
修饰符在视图树上传递数据。
swift
import SwiftUI
struct ChildView: View {
@State private var alert: PresentableAlert?
var body: some View {
ZStack {
Color.orange
VStack {
Button("显示对话框", action: {
self.alert = PresentableAlert(title: "标题", message: "温馨提示") })
// 传递值
.preference(key: AlertPreferenceKey.self, value: alert)
}
}
}
}
- 使用
onPreferenceChange
修饰符来获取当前视图树中的数据。
swift
import SwiftUI
struct ContentView: View {
@State private var alert: PresentableAlert?
var body: some View {
ChildView()
// 获取值并保存到alert中
.onPreferenceChange(AlertPreferenceKey.self) { self.alert = $0 }
.alert(item: $alert) { alert in
Alert(title: Text(alert.title), message: Text(alert.message))
}
}
}
总结
Preference 是 SwiftUI 中一个极其强大但也容易被忽视的功能,虽然用法稍显繁琐,但它能大幅拓展 SwiftUI 的表达力,尤其是在自定义复杂组件时。它适用于以下场景:
- 子视图需要向上传递状态。
- 子视图尺寸、布局等信息需要传递给容器。
- 深层视图希望通知祖先视图执行某些操作(例如弹窗、导航跳转等)。