• 作者:老汪软件技巧
  • 发表时间:2024-08-19 10:05
  • 浏览量:

本文章参考自 Apple 官方文档

版本兼容性:iOS17.0+ | macOS14.0+ | visionOS1.0+ | iPadOS17.0+

Observation框架主要是在 WWDC23 进行发布,它是 SwiftUI 和其他 Swift 应用程序中数据绑定和观察属性变化的简化工具。下面我主要从以下方面进行介绍:

主要特性如何声明可观察对象如何在视图中使用可观察对象可观察对象在视图间如何传递如何跟踪可观察对象的属性变化

其实在 Swift5.9之前使用的响应式状态管理框架可能都是 Combine 框架,但是为什么官方还会推出这个新的框架,主要就是苹果没有为开发者提供一种统一高效的机制来观察引用类型属性对变化。KVO 仅限于 NSObject 子类使用,Combine 无法提供属性级别的精确观察,而且两者都无法实现跨平台支持。

此外,在 SwiftUI 中,引用类型的数据源(Source of Truth)采用了基于 Combine 框架的 ObservableObject 协议实现。这导致在 SwiftUI 中,极易产生了大量不必要的视图刷新,从而影响 SwiftUI 应用的性能。

为了改善这些限制,Swift 5.9 版本推出了 Observation 框架。相比现有的 KVO 和 Combine,它具有以下优点:

适用于所有 Swift 引用类型,不限于 NSObject 子类,提供跨平台支持。提供属性级别的精确观察,且无需对可观察属性进行特别注解。减少 SwiftUI 中对视图的无效更新,提高应用性能。主要特性如何声明可观察对象

//定义可观察对象
@Observable class PreviewsModel {
    var cards: [String] = ["Feng", "BBA"]
    //计算属性
    var description: String {
        cards.joined(separator: ",")
    }
}
@Observable class ContentModel {
    @ObservationIgnored var description: String = ["Feng", "BBA"].joined(separator: ",")
}

定义 @Observable 可观察对象,在类的声明前添加 @Observable 标识即可,无需让定义的类型去遵循什么协议。可以观察计算属性,例如示例代码中的 description 计算属性。如果属性不想被跟踪或者观察,那么可以使用 @ObservationIgnored 标识符标记。

注意:目前我们使用的 @Observable 符号其实 Swift 中的宏,它主要是支持开发者在编译阶段去执行额外的代码比如添加一些执行的代码等操作。

在 Xcode 15 中,在@Observable处点击鼠标右键,选择“Expand Macro”操作。通过这步操作,我们可以看到@Observable宏为我们生成的代码:

@Observable class PreviewsModel {
    @ObservationTracked
    var cards: [String] = ["Feng", "BBA"]
    var description: String {
        cards.joined(separator: ",")
    }
    @ObservationIgnored private let _$observationRegistrar = Observation.ObservationRegistrar()
    internal nonisolated func access<Member>(keyPath: KeyPath<PreviewsModel, Member>) {
        _$observationRegistrar.access(self, keyPath: keyPath)
    }
    internal nonisolated func withMutation<Member, MutationResult>(keyPath: KeyPath<PreviewsModel, Member>,_ mutation: () throws -> MutationResult)rethrows -> MutationResult {
        try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
    }
}
extension PreviewsModel: Observation.Observable {}

以上宏主要操作就是通过 Observation 的 Observationegistrar 结构进行属性或者方法的注册以及管理,在 get 和 set 方法中,通过_$observationRegistrar来注册和通知观察者。最后,宏添加了让可观察对象遵守 Observable 协议的代码(Observable 协议类似于 Sendable, 它不提供任何实现,仅起标示作用)。

由于其实宏的操作中已经使用 @ObservationTracked 定义可观察属性,所以使用 @Observable 定义可观察对象时再次使用 @ObservationTracked 会出现报错!

注意:其实这里有个 Key-Path 这是 swift 中的概念:键路径(Key-Path).Key-Path 表达式是 Swift 中的一种强大功能,用于访问对象的属性或方法。这种表达式提供了一种类型安全的方式来引用类型的属性,而无需在编译时知道这些属性的名称。后面会进行介绍!

如何在视图中使用可观察对象在视图中声明可观察对象

在新的 SwiftUI 中,我们在视图中使用 @State 去定义可观察对象的实例,以确保可观察对象的生命周期跟随视图的生命周期。

//定义可观察模型类
@Observable class ContentModel {
    @ObservationIgnored **var** description: String = ["Feng", "BBA"].joined(separator: ",")
}
@main
struct observationframestudyApp: App {
    @State var previewModel = PreviewsModel()
    @State var contentModel = ContentModel()
    var body: some Scene {
        ...
    }
}

通过环境变量注入的方式可观察对象

注意:为什么采用环境变量的方式,其实这种方式相对更加方便,除了这种方式还有另一种方式!可以根据文件的结构层级或者结构的代码层级通过参数的方式一层一层根据你的需求去进行传递,但是这种方式如果是对于不清楚需要传递到哪一层级的情况或者嵌套太深就是比较麻烦的事情,也不利于后续自己的维护。后面也会介绍层级间传递的情况以及如何数据双向的绑定。

通过环境注入可观察对象主要两种方式:

通过 .environment(_:)通过 .environment(_:,_:) 初始化方法需要两个参数,详细可以点击链接查看官方文档介绍:

nonisolated 
func environment<V>(
    _ keyPath: WritableKeyPath<EnvironmentValues, V>,
    _ value: V 
)  -> some View

虽然这两种方法都是通过 .environment() 方法去进行传递参数给到环境中,但是后者需要自定义 EnvironmentKey 以及配置 EnvironmentValues。

//定义可观察对象
@Observable class PreviewsModel {
    ...
}
@Observable class ContentModel {
    ...
}
struct ObservationTest: App {
   @State var previewModel = PreviewsModel()
   @State var contentModel = ContentModel()
   var body: some Scene {
        WindowGroup {
            ContentView()
                //如果通过下面这种方式注入两个环境变量,后续读取需要根据定义的名字进行读取
                .environment(previewModel)
                .environment(contentModel)
        }
    }
}
struct ContentView: View {
    @Environment(\.previewModel) private var previewModel
    @Environment(\.contentModel) private var contentModel // 在视图中通过环境注入
    var body: some View {
       ...
    }
}

//定义可观察对象
@Observable class PreviewsModel {
    ...
}
@Observable class ContentModel {
    @ObservationIgnored var description: String = ["Feng", "BBA"].joined(separator: ",")
}
@main
struct observationframestudyApp: App {
    @State var previewModel = PreviewsModel()
    @State var contentModel = ContentModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.previewModel, previewModel)
                .environment(\.contentModel, contentModel)
        }
    }
}
extension EnvironmentValues {
    var previewModel: PreviewsModel {
        get {
            return self[PreviewsModelKey.self]
        }
        set { self[PreviewsModelKey.self] = newValue }
    } 
    var contentModel: ContentModel {
        get { self[ContentModelKey.self] }
        set { self[ContentModelKey.self] = newValue }
    }
}
private struct PreviewsModelKey: EnvironmentKey {
    //实现这个协议的静态 defaultValue属性默认值
    static var defaultValue: PreviewsModel = PreviewsModel()
}
private struct ContentModelKey: EnvironmentKey {
    static var defaultValue: ContentModel = ContentModel()
}

可观察对象在视图间如何传递

struct ContentView: View {
    @State var store = Store()
    var body: some body {
        SubView(store: store)
    }
}
struct SubView:View {
    let store:Store
    var body: some body {
       ....
    }
}

不管是 let 还是 var 关键字定义的都可以。

创建Binding类型

Binding 类型为 SwiftUI 提供了实现数据双向绑定的能力。使用 Observation 框架,我们可以通过如下方式创建属性对应的 Binding 类型。

struct ContentView: View {
    @State var store = Store()
    var body: some body {
        SubView(store: store)
    }
}
struct SubView:View {
    @Bindable var store:Store
    var body: some body {
       TextField("",text:$store.name)
    }
}

struct SubView:View {
    var store:Store
    var body: some body {
       @Bindable var store = store
       TextField("",text:$store.name)
    }
}

如何跟踪可观察对象的属性变化

跟踪可观察对象的变化我们主要通过 withObservationTracking 方法进行实现。

withObservationTracking 主要用于在某个代码块中跟踪属性的变化,并执行相应的回调。这对于需要响应属性变化的场景非常有用。 下面是示例代码演示。

import SwiftUI
import Observation
@Observable class Counter {
    @ObservationTracked var count = 0
}
struct ContentView: View {
    @State private var counter = Counter()
    var body: some View {
        VStack {
            Text("Count: \(counter.count)")
                .font(.largeTitle)
            Button(action: {
                incrementCounter()
            }) {
                Text("Increment")
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
            }
        }
        .padding()
    }
    private func incrementCounter() {
        // 使用 withObservationTracking 跟踪属性变化
        withObservationTracking {
            counter.count += 1
        } onChange: {
            print("Counter changed to \($0)")
        }
    }
}
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

上面代码中 withObservationTracking方法会对包裹的引用的可观察对象的变量进行跟踪,但引用的可观察对象数值变化那么就会触发 onChange 方法

以下是基于上面的知识部分涉及到的概念讲解:

不得不知的知识一:Key-Path 表达式

Key-Path 表达式是 Swift 中的一种强大功能,用于访问对象的属性或方法。这种表达式提供了一种类型安全的方式来引用类型的属性,而无需在编译时知道这些属性的名称。

Key-Path 表达式使用点语法访问类型的属性。通过使用 \TypeName.property 语法,你可以创建一个 Key-Path,这个 Key-Path 可以被传递和存储,以便稍后在运行时使用。

基本语法

Key-Path 表达式的基本语法如下:

let keyPath = \TypeName.property

其中 TypeName 是类型名称,property 是该类型的属性名称。

让我们通过一个简单的示例来演示 Key-Path 表达式的使用。

定义一个结构体

struct Person {
    var name: String
    var age: Int
}

创建 Key-Path 表达式

let nameKeyPath = \Person.name
let ageKeyPath = \Person.age

使用 Key-Path 表达式

你可以通过 Key-Path 表达式访问和修改属性。

var person = Person(name: "Alice", age: 30)
// 访问属性
let name = person[keyPath: nameKeyPath]
print(name) // 输出:Alice
// 修改属性
person[keyPath: ageKeyPath] = 31
print(person.age) // 输出:31

高级用法

Key-Path 表达式不仅可以访问简单属性,还可以用于访问嵌套属性和集合属性。

嵌套属性

struct Address {
    var city: String
    var zipCode: String
}
struct Person {
    var name: String
    var age: Int
    var address: Address
}
let cityKeyPath = \Person.address.city
var person = Person(name: "Alice", age: 30, address: Address(city: "New York", zipCode: "10001"))
// 访问嵌套属性
let city = person[keyPath: cityKeyPath]
print(city) // 输出:New York

集合属性

struct Team {
    var members: [Person]
}
let firstMemberNameKeyPath = \Team.members[0].name
var team = Team(members: [Person(name: "Alice", age: 30), Person(name: "Bob", age: 25)])
// 访问集合属性
let firstMemberName = team[keyPath: firstMemberNameKeyPath]
print(firstMemberName) // 输出:Alice
// 修改集合属性
team[keyPath: firstMemberNameKeyPath] = "Charlie"
print(team.members[0].name) // 输出:Charlie

使用场景

Key-Path 表达式在许多场景中都非常有用,以下是几个常见的使用场景:

数据绑定:在数据绑定框架中,通过 Key-Path 表达式可以方便地绑定属性。排序和过滤:在使用集合方法(如 sort 和 filter)时,可以使用 Key-Path 表达式来指定排序或过滤的属性。泛型编程:在泛型函数中,Key-Path 表达式提供了一种类型安全的方式来访问属性。示例:在排序中使用 Key-Path 表达式

var people = [
    Person(name: "Alice", age: 30),
    Person(name: "Bob", age: 25),
    Person(name: "Charlie", age: 35)
]
// 使用 Key-Path 表达式进行排序
let sortedPeople = people.sorted(by: \Person.age)
print(sortedPeople.map { $0.name }) // 输出:["Bob", "Alice", "Charlie"]

Key-Path 表达式是 Swift 中一个强大且灵活的功能,提供了一种类型安全、简洁的方式来访问和操作属性。在使用数据绑定、排序、过滤和泛型编程时,Key-Path 表达式可以极大地简化代码,提高代码的可读性和安全性。

文档持续更新,欢迎指点问题...