SwiftUI 作为一个响应式框架,其视图需要随着状态的变化而更新。这就要求它依赖于某种机制来追踪这些状态变化。Observation 框架核心目标是为 SwiftUI 引入一个全新的观察机制,以此来提升 SwiftUI 应用的性能。在引入 Observation 框架之前,SwiftUI 实际上已经有了两套不同的观察机制…

参考链接:


Combine 旧框架

实际上,Combine 观察模式在当前的 SwiftUI 中存在一些问题。它会导致大量视图无效更新…

final class MyObservableObject: ObservableObject {
		@Published var name: String
		@Published var age: Int
		// 更多属性...
}

extension MyObservableObject {
		var objectWillChange: Self.ObjectWillChangePublisher // 内部发布者实例,视图将订阅这个发布者
}

当使用 Combine 框架,并声明类型为 ObservableObject 的可观察对象时,首先需要的是符合 ObservableObject 协议。这个协议会在类中创建一个发布者实例。每当有属性被 @Published 修饰符标记的变量发生改变时,@Published 会触发类中的这个发布者,向所有订阅者发送通知。然而,因为这些通知不包含改变的具体信息,订阅者无法得知是哪个 @Published 标记的属性发生了变化。

在 SwiftUI 中,与可观察对象相关联的视图就是这些订阅者。即使是可观察对象中极小的一部分状态发生变化,也会导致所有相关联的视图进行更新,进而造成了大量的无效视图更新,严重影响了应用的性能。


// 例如:这里有一个具有两个属性的可观察对象
final class Store: ObservableObject {
		@Published var name = "肥肥"
		@Published var age = 5
}

// 在两个不同的子视图中,我们分别引用了这个对象,并且每个视图只使用了其中一个属性:
struct NameView: View {
		@ObservedObject var store: Store
		var body: some View {
				let _ = print("NameView Update")
				Text(store.name)
		}
}

struct AgeView: View {
		@ObservedObject var store: Store
		var body: some View {
				let _ = print("AgeView Update")
				Text(store.age, format: .number)
		}
}

由于 NameView 视图与 store 实例建立了联系(即响应该实例发出的通知),因此,即便是 age 属性发生变化时,它也会被更新。这就揭示了当前观察机制的不足之处。正是基于这个考虑,苹果公司推出了 Observation 框架。


Observation 新框架(iOS 17)

Observation 框架为开发者提供了两个主要工具:@Observable 宏和 withObservationTracking 函数。宏在 Swift 5.9 中引入,旨在减轻开发者的负担,避免手动编写重复的代码,从而提高开发效率。

这种观察逻辑看起来独特,实际上是为满足 SwiftUI 的特定需求而精心设计的。它形成了一个完美的闭环:从“创建观察”(将视图的 body 放入 apply),到“状态变化”,再到“视图更新”(调用 onChange 闭包),最后“重新创建观察”(重复之前的步骤),这一系列操作紧密相连,适应了 SwiftUI 的渲染和更新机制。


新框架的优点

Observation 框架是 SwiftUI 5 中的一个重大创新,它完全改变了 SwiftUI 的响应式基础。Observation 框架带来了许多好处:

<aside> 💡 如果您的应用当前因观察精度不足而遭受大量无效视图更新和性能下降的问题,仅需将其转换为 Observation 方式,便可迅速看到明显的改善。想要深入了解 Observation 框架的具体使用方法和更多细节,推荐阅读 深度解读Observation——SwiftUI性能提升的新途径

</aside>

新框架的特点

Observation 框架所提供的观察机制展现了几个独特的特点:


Observation 的新特性

支持可观察对象嵌套

Observation 提供了一种全新的构建和组织应用状态的方式。现在可以将一个可观察对象嵌套在另一个对象中,实现以前难以构建的状态关系。在以前 Combine 框架下,尝试实现这种嵌套会面临很大的挑战,因为 @Published 属性不支持观察引用类型内部的变化。

@Observable
class A {
		var b = 1
}

@Observable
class B {
		var b = 1
		//将一个可观察对象作为另一个可观察对象的属性
		var a = A() 
}

精准的观察构建逻辑

前向兼容性

Observation 框架的另一个关键优点是:前向兼容性(Back-porting)。


从 Combine 迁移到 Observation

原 Combine 机制代码

import SwiftUI
import Combine

// 1.创建一个符合 ObservableObject 协议的类
class UserData: ObservableObject {

		// 2.使用 @Published 标注可观察属性
    @Published var username = "Guest"
    @Published var age = 25
}

//【根页面】
struct ContentView: View {

		// 3.使用时用 @ObservedObject 实作
    @ObservedObject var userData = UserData()
    
    var body: some View {
        VStack {
            Text("Username: \\\\(userData.username)")
            Text("Age: \\\\(userData.age)")
            Button("Update Username") {
                userData.username = "NewUser"
            }
            Button("Increase Age") {
                userData.age += 1
            }
        }
        // 4.使用 .environmentObject 方法将 userData 注入到环境中
        .environmentObject(userData)
    }
    
}

//【其他页面】
struct DetailView: View {

		// 5.应用的其他页面通过 @EnvironmentObject 获取环境对象
    @EnvironmentObject var userData: UserData

    var body: some View {
        VStack {
            Text("Detail View")
            Text("Username: \\\\(userData.username)")
            Text("Age: \\\\(userData.age)")
            Button("Update Username") {
                userData.username = "UpdatedUser"
            }
        }
    }
    
}

要将依赖 Combine 的可观察机制迁移到依赖 Observation 框架的机制,需要进行几项关键的调整:

迁移后的 Observation 代码

import SwiftUI
import Observation

// 1.移除 ObservableObject 协议,直接使用 @Observable 宏来使整个类的属性具有观察性
@Observable class UserData {

		// 2.移除 @Published:新机制中所有存储属性都会自动被观察,不需要额外的 @Published 标记
    var username = "Guest"
    var age = 25
}

//【根页面】
struct ContentView: View {

		// 3.用 @State 来替换原本的 @StateObject
    @State var userData = UserData()

    var body: some View {
        VStack {
            Text("Username: \\\\(userData.username)")
            Text("Age: \\\\(userData.age)")
            Button("Update Username") {
                userData.username = "NewUser"
            }
            Button("Increase Age") {
                userData.age += 1
            }
        }
        // 4.使用新的环境注入方法( 从 .environmentObject 变为了 .environment)
        .environment(userData)  
    }
}

//【其他页面】
struct DetailView: View {

		// 5. 访问环境对象的方法从 @EnvironmentObject 变为 @Environment,并且需要传递类型 UserData.self
    @Environment(UserData.self) var userData

    var body: some View {
        VStack {
            Text("Detail View")
            Text("Username: \\\\(userData.username)")
            Text("Age: \\\\(userData.age)")
            Button("Update Username") {
                userData.username = "UpdatedUser"
            }
        }
    }
}