Swift 中的 withObservationTracking 完全指南

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

工作机制

  1. 执行 apply 闭包,系统自动记录闭包中读取了哪些属性
  2. 当这些属性中的任何一个发生变化时,执行 onChange 闭包
  3. 返回 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 更新
不适用 需要复杂过滤、需要历史值的场景

核心要点

  1. 属性必须属于 @Observable才能被观察
  2. 只跟踪 apply 闭包中读取的属性
  3. 需要手动重新调用来实现持续观察
  4. onChange 没有参数,无法知道哪个属性变化
  5. 注意内存管理,使用 [weak self]

withObservationTracking 是 Swift 迈向更简洁响应式编程的重要一步,它让属性观察变得前所未有的简单。虽然有一些限制(特别是无法获取变化详情),但在大多数 UI 同步场景中完全够用。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容