withObservationTracking 一分钟速通
一句话总结
自动监听属性变化,变了就执行代码。
核心用法
// 1. 类要加 @Observable
@Observable
class User {
var name = ""
}
// 2. 监听变化
withObservationTracking {
print(user.name) // 读取哪个属性,就监听哪个
} onChange: {
print("name变了") // 变化时执行
}
两个关键点
| 关键点 | 说明 |
|---|---|
| 只监听读取过的属性 | 闭包里写了 user.name,就只监听 name
|
| 只触发一次 | 变化后自动停止,需要自己重新调用 |
持续监听写法
func watch() {
withObservationTracking {
print(user.name)
} onChange: {
watch() // 重新调用,继续监听
}
}
对比传统方式
| 方式 | 代码量 | 难度 |
|---|---|---|
| KVO | 多 | 复杂 |
| Combine | 多 | 复杂 |
| withObservationTracking | 少 | 简单 |
一句话记住
在闭包里读什么,就监听什么;变了就执行 onChange,然后停止。
详细说明👇👇👇👇👇👇
一、什么是 withObservationTracking?
withObservationTracking 是 Swift 5.9(iOS 17+)引入的观察框架(Observation Framework)中的核心 API。它提供了一种声明式的方式来观察属性的变化,当被观察的属性值发生变化时,自动执行指定的回调。
核心概念
func withObservationTracking<T>(
_ apply: () -> T, // 跟踪闭包:读取需要观察的属性
onChange: @autoclosure () -> @Sendable () -> Void // 变化闭包:属性变化时执行
) -> T
工作机制:
- 执行
apply闭包,系统自动记录闭包中读取了哪些属性 - 当这些属性中的任何一个发生变化时,执行
onChange闭包 - 返回
apply闭包的执行结果
二、为什么需要 withObservationTracking?
传统 KVO 的痛点
// 传统 KVO 需要手动注册/移除
class Person: NSObject {
@objc dynamic var name: String = ""
}
class ViewController {
var person: Person?
var observation: NSKeyValueObservation?
func observe() {
observation = person?.observe(\.name, options: [.new]) { _, change in
print("Name changed to \(change.newValue ?? "")")
}
}
deinit {
observation?.invalidate() // 必须手动移除
}
}
问题:
- 需要手动注册和移除观察者
- 容易造成内存泄漏
- 不支持结构体
- 只能观察
@objc dynamic属性
Combine 的复杂度
// Combine 需要理解 Publisher/Subscriber 概念
class ViewModel {
@Published var name: String = ""
var cancellables = Set<AnyCancellable>()
func observe() {
$name
.sink { newValue in
print("Name changed to \(newValue)")
}
.store(in: &cancellables)
}
}
问题:
- 学习曲线陡峭
- 需要管理 Cancellable
- 代码量较大
withObservationTracking 的优势
// ✅ 简单直接
func observe() {
withObservationTracking {
print(person.name) // 自动观察 name
} onChange: {
print("Name changed")
}
}
三、基本用法
1. 观察单个属性
import Observation
@Observable
class User {
var name: String = ""
var age: Int = 0
}
let user = User()
func observeUserName() {
withObservationTracking {
print("Current name: \(user.name)") // 只观察 name
} onChange: {
print("user.name 发生了变化!")
}
}
observeUserName()
user.name = "张三" // 触发 onChange
user.age = 18 // 不触发 onChange(未观察 age)
2. 观察多个属性
func observeUserBoth() {
withObservationTracking {
// 同时读取 name 和 age,两者都会被观察
let name = user.name
let age = user.age
print("User: \(name), \(age) years old")
} onChange: {
print("name 或 age 发生了变化")
}
}
user.name = "李四" // 触发
user.age = 20 // 也触发
3. 返回值
let result = withObservationTracking {
return user.name.uppercased()
} onChange: {
print("name changed")
}
print(result) // 输出 name 的大写形式
4. 持续观察(循环模式)
func startObserving() {
withObservationTracking {
print(user.name)
} onChange: {
print("检测到变化,重新建立观察")
startObserving() // 重新调用,形成持续观察
}
}
四、观察类的要求
使用 @Observable 宏(推荐)
import Observation
@Observable
class ViewModel {
var title: String = ""
var isLoading: Bool = false
var items: [String] = []
}
let vm = ViewModel()
withObservationTracking {
print(vm.title) // ✅ 可观察
print(vm.isLoading) // ✅ 可观察
print(vm.items) // ✅ 可观察
} onChange: {
print("属性变化")
}
不使用 @Observable(有限支持)
// ❌ 普通类不会触发观察
class PlainClass {
var value: Int = 0
}
let plain = PlainClass()
withObservationTracking {
print(plain.value) // 不会建立观察
} onChange: {
print("不会执行")
}
五、onChange 闭包详解
基本形式
withObservationTracking {
print(user.name)
} onChange: {
print("变化了") // 无参数
}
如何知道哪个属性变化了?
onChange 闭包没有参数,因此无法直接知道哪个属性变化。
// ❌ onChange 没有参数,无法获取变化信息
withObservationTracking {
print("\(user.name) - \(user.age)")
} onChange: {
// 不知道是 name 还是 age 变化了
}
解决方案:在 onChange 中重新读取所有值
var lastKnownName = user.name
var lastKnownAge = user.age
func observe() {
withObservationTracking {
let currentName = user.name
let currentAge = user.age
print("\(currentName) - \(currentAge)")
} onChange: {
// 对比当前值和上次记录的值,找出变化的属性
if user.name != lastKnownName {
print("name 变化了: \(lastKnownName) -> \(user.name)")
lastKnownName = user.name
}
if user.age != lastKnownAge {
print("age 变化了: \(lastKnownAge) -> \(user.age)")
lastKnownAge = user.age
}
observe() // 重新观察
}
}
六、实际应用场景
1. UI 自动刷新
class UserProfileViewController: UIViewController {
@Observable
class ViewModel {
var username: String = ""
var avatarURL: String = ""
}
var viewModel = ViewModel()
override func viewDidLoad() {
super.viewDidLoad()
startObservingViewModel()
}
private func startObservingViewModel() {
withObservationTracking { [weak self] in
guard let self = self else { return }
// 读取需要响应的属性
let name = self.viewModel.username
let avatar = self.viewModel.avatarURL
print("Updating UI with: \(name), \(avatar)")
// 实际更新 UI 的代码
self.updateUI(name: name, avatar: avatar)
} onChange: { [weak self] in
DispatchQueue.main.async {
self?.startObservingViewModel()
}
}
}
private func updateUI(name: String, avatar: String) {
// 更新 UI
}
}
2. 多属性依赖计算
@Observable
class ShoppingCart {
var items: [Item] = []
var discount: Double = 0
var shipping: Double = 5.0
}
class CartViewModel {
var cart = ShoppingCart()
var totalPrice: Double = 0
func startObserving() {
withObservationTracking { [weak self] in
guard let self = self else { return }
// 读取所有影响总价的属性
let itemsTotal = self.cart.items.reduce(0) { $0 + $1.price }
let discount = self.cart.discount
let shipping = self.cart.shipping
// 重新计算总价
self.totalPrice = itemsTotal * (1 - discount) + shipping
print("Total price updated: \(self.totalPrice)")
} onChange: { [weak self] in
DispatchQueue.main.async {
self?.startObserving() // 重新建立观察
}
}
}
}
3. 网络请求触发
@Observable
class SearchViewModel {
var searchText: String = ""
var searchResults: [String] = []
}
class SearchService {
var viewModel = SearchViewModel()
func setupSearch() {
withObservationTracking { [weak self] in
guard let self = self else { return }
let query = self.viewModel.searchText
print("Search triggered for: \(query)")
// 发起网络请求
Task {
let results = await self.performSearch(query: query)
await MainActor.run {
self.viewModel.searchResults = results
}
}
} onChange: { [weak self] in
DispatchQueue.main.async {
self?.setupSearch()
}
}
}
func performSearch(query: String) async -> [String] {
// 模拟网络请求
try? await Task.sleep(nanoseconds: 500_000_000)
return ["Result for: \(query)"]
}
}
七、与 SwiftUI 的集成
SwiftUI 的 @State、@ObservedObject 等属性包装器底层已经集成了观察机制,通常不需要手动使用 withObservationTracking。
// ✅ SwiftUI 自动处理
struct ContentView: View {
@State private var user = User()
var body: some View {
Text(user.name) // 自动观察
}
}
// ⚠️ 手动使用(通常不需要)
struct ManualView: View {
@State private var user = User()
var body: some View {
Text(user.name)
.onAppear {
withObservationTracking {
print(user.name)
} onChange: {
print("User changed")
}
}
}
}
八、性能注意事项
1. 避免频繁重新建立观察
// ❌ 不好:每次变化都立即重新建立,可能造成性能问题
func badObserve() {
withObservationTracking {
print(user.name)
} onChange: {
badObserve() // 立即重新建立
}
}
// ✅ 好:使用防抖或节流
func goodObserve() {
withObservationTracking {
print(user.name)
} onChange: {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
goodObserve() // 延迟重新建立
}
}
}
2. 避免在 onChange 中做耗时操作
// ❌ 不好
func observe() {
withObservationTracking {
print(user.name)
} onChange: {
// 耗时操作会阻塞 onChange 的后续调用
heavyComputation()
observe()
}
}
// ✅ 好
func observe() {
withObservationTracking {
print(user.name)
} onChange: {
Task.detached { // 在后台线程执行
await heavyComputation()
await MainActor.run {
observe()
}
}
}
}
九、常见问题与陷阱
1. 观察不会自动持续
// ❌ 只会触发一次
func observeOnce() {
withObservationTracking {
print(user.name)
} onChange: {
print("Changed") // 只触发一次
}
}
// ✅ 需要手动重新调用
func observePersistent() {
withObservationTracking {
print(user.name)
} onChange: {
print("Changed")
observePersistent() // 手动重新建立
}
}
2. 闭包中的属性才会被观察
@Observable
class Data {
var a: Int = 0
var b: Int = 0
}
let data = Data()
// 只观察 a
withObservationTracking {
print(data.a) // 这里读取了 a
} onChange: {
print("Changed")
}
data.b = 10 // 不会触发
data.a = 5 // 会触发
3. 循环引用问题
class ViewController: UIViewController {
@Observable
class ViewModel {
var value: Int = 0
}
var viewModel = ViewModel()
func setup() {
// ❌ 可能造成循环引用
withObservationTracking {
print(self.viewModel.value) // 强引用 self
} onChange: {
self.setup() // 强引用 self
}
// ✅ 使用 weak self
withObservationTracking { [weak self] in
print(self?.viewModel.value ?? 0)
} onChange: { [weak self] in
self?.setup()
}
}
}
十、版本兼容性
| 平台 | 最低版本 |
|---|---|
| iOS | 17.0+ |
| macOS | 14.0+ |
| watchOS | 10.0+ |
| tvOS | 17.0+ |
向下兼容方案:
if #available(iOS 17.0, macOS 14.0, *) {
withObservationTracking {
print(user.name)
} onChange: {
print("Changed")
}
} else {
// 使用 KVO 或 Combine 作为降级方案
// 或使用 NotificationCenter
}
十一、总结
| 特性 | 说明 |
|---|---|
| 用途 | 观察属性变化,自动执行回调 |
| 优点 | 简洁、类型安全、自动管理 |
| 缺点 | 需要 iOS 17+,无法获取变化详情 |
| 适用场景 | 响应式编程、状态同步、UI 更新 |
| 不适用 | 需要复杂过滤、需要历史值的场景 |
核心要点
-
属性必须属于
@Observable类才能被观察 - 只跟踪
apply闭包中读取的属性 - 需要手动重新调用来实现持续观察
-
onChange没有参数,无法知道哪个属性变化 -
注意内存管理,使用
[weak self]
withObservationTracking 是 Swift 迈向更简洁响应式编程的重要一步,它让属性观察变得前所未有的简单。虽然有一些限制(特别是无法获取变化详情),但在大多数 UI 同步场景中完全够用。