SwiftUI-Preference

在 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)
        }
    }
}
  • PreferenceEnvironment功能类似,都可以跨视图传递数据。不同的是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

使用步骤

  1. 定义 Preference 的类型,即 Value 的类型。
  2. 定义 defaultValue,如果在 View 的层次结构中从未设置过值,则使用该值。
  3. 实现 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 的表达力,尤其是在自定义复杂组件时。它适用于以下场景:

  • 子视图需要向上传递状态。
  • 子视图尺寸、布局等信息需要传递给容器。
  • 深层视图希望通知祖先视图执行某些操作(例如弹窗、导航跳转等)。
相关推荐
HarderCoder4 小时前
ByAI:iOS 生命周期:AppDelegate 与 SceneDelegate 中的 `willEnterForeground` 方法解析
swift
大熊猫侯佩4 小时前
SwiftData 如何在 Widgets 和 App 的界面之间同步数据变化?
swiftui·swift·apple watch
HarderCoder4 小时前
ByAI:使用DRY原则编写干净可复用的Swift代码
swift
season_zhu6 小时前
Swift:优雅又强大的语法糖——Then库
ios·架构·swift
东坡肘子6 小时前
Swift 新设计、新案例、新体验 | 肘子的 Swift 周报 #087
swiftui·swift·wwdc
大熊猫侯佩1 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩1 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(四)
数据库·swiftui·apple watch
MaoJiu2 天前
Flutter造轮子系列:flutter_permission_kit
flutter·swiftui