如何基于Swift Observation框架按需高效更新ExerciseSearchModel的filteredExercises属性
嘿,我完全懂你现在的困惑——刚从Combine或者老的ObservableObject切换到Swift Observation框架时,确实容易在这种“按需更新计算属性”的场景下卡壳。你提到的计算属性频繁重算、Combine不符合未来技术栈的顾虑都非常合理,其实用Observation框架有一套非常优雅且高效的方案,完全贴合Apple的最新设计思路,我这就给你拆解清楚。
核心思路:缓存+依赖追踪+手动变更通知
我们的目标是:只有当exercises、searchQuery、filters、sort中任意一个属性变化时,才重新计算filteredExercises,并且用缓存存储结果,避免重复计算。Observation框架本身提供了精确的属性追踪能力,我们只需要结合缓存和手动触发视图更新就能实现。
具体代码实现
直接修改你的ExerciseSearchModel,核心改动是用缓存属性存储计算结果,通过Observation的追踪机制监听依赖变化,按需更新缓存并通知视图:
import Observation @Observable public class ExerciseSearchModel: RouteAware { // 原始可观察属性(框架自动追踪变化) public var exercises: [Exercise] = [] public var searchQuery: String = "" public var filters: ExerciseFilters = .init() public var sort: ExerciseSort = .defaultValue // 缓存计算结果:用@ObservationIgnored忽略自动观察,我们手动管理更新时机 @ObservationIgnored private var _filteredExercises: IdentifiedArrayOf<Exercise> = [] // 对外暴露的只读属性,直接返回缓存值 public var filteredExercises: IdentifiedArrayOf<Exercise> { get { // 初始化兜底:如果缓存为空且有数据,先计算一次初始值 if _filteredExercises.isEmpty && !exercises.isEmpty { updateFilteredExercises() } return _filteredExercises } } // 假设你有FilterExercisesUseCase的引用,这里先声明 private let filterExercisesUseCase: FilterExercisesUseCase public init(filterExercisesUseCase: FilterExercisesUseCase) { self.filterExercisesUseCase = filterExercisesUseCase // 初始化时启动依赖监听 setupDependencyTracking() } // 设置依赖属性的变化监听 private func setupDependencyTracking() { withObservationTracking { // 访问所有需要追踪的依赖属性,告诉框架要监听它们的变化 _ = self.exercises _ = self.searchQuery _ = self.filters _ = self.sort } onChange: { [weak self] in // 当任何一个依赖属性变化时,重新计算并更新缓存 self?.updateFilteredExercises() // 手动发送变更通知,让视图知道filteredExercises已更新 self?.objectWillChange.send() // 重新建立监听(withObservationTracking是单次的,触发后需要重置) self?.setupDependencyTracking() } } // 抽离实际的计算逻辑,代码更清晰 private func updateFilteredExercises() { guard !exercises.isEmpty else { _filteredExercises = [] return } let result = filterExercisesUseCase.execute( exercises: exercises, query: searchQuery, filters: filters, sort: sort ) _filteredExercises = IdentifiedArrayOf(uniqueElements: result) } }
关键细节解释
@ObservationIgnored的作用:
我们把缓存用的_filteredExercises标记为@ObservationIgnored,是因为我们不想让框架自动追踪这个属性的变化——毕竟它是我们手动更新的缓存,我们要自己控制什么时候通知视图更新,而不是让框架自动触发不必要的刷新。withObservationTracking的用法:
这个API是Observation框架的核心之一,它允许我们精确监听指定属性的变化。我们在闭包里访问四个依赖属性,框架就会自动记录这些依赖;当其中任何一个变化时,onChange闭包就会触发。
注意:withObservationTracking是单次监听,触发后就会失效,所以我们需要在onChange里再次调用setupDependencyTracking()来重新建立监听,这样就能持续追踪后续的变化。手动发送
objectWillChange:
因为我们更新的是@ObservationIgnored的缓存属性,框架不会自动通知视图变化,所以需要手动调用objectWillChange.send()——这会告诉所有观察这个模型的视图:“嘿,有属性更新了,赶紧刷新!”,视图就会重新读取filteredExercises的缓存值。缓存的初始化处理:
在filteredExercises的getter里,我们做了一个兜底:如果缓存为空但exercises有数据,就先计算一次。这样第一次访问filteredExercises时,就能得到正确的初始值,不需要等到第一次依赖变化。
为什么这个方案比你之前的尝试更好?
- 完全贴合Observation框架:没有用Combine,完全基于Apple最新的响应式方案,符合未来的技术栈方向。
- 按需计算,无冗余:只有当四个依赖属性中的任何一个变化时,才会重新计算
filteredExercises,彻底解决了计算属性每次访问都重算的问题。 - 缓存机制高效:视图每次访问
filteredExercises都是直接取缓存,不会触发计算,性能拉满。 - 代码简洁易维护:所有依赖追踪逻辑都集中在
setupDependencyTracking()里,不需要给每个属性加didSet,也不需要写Combine的combineLatest,代码更干净。
额外注意事项
- 如果
ExerciseFilters或ExerciseSort是引用类型(而不是值类型struct),那你需要把它们也标记为@Observable,这样Observation框架才能追踪它们内部属性的变化。不过从你的代码看,它们应该是值类型,所以没问题。 - 一定要用
weak self在onChange闭包里,避免循环引用导致内存泄漏。 - 如果你的
filterExercisesUseCase.execute()是异步方法,那你需要把updateFilteredExercises()改成异步的,并且在异步回调里更新缓存后再发送objectWillChange。不过从你的描述看,这个方法是同步的,所以当前写法没问题。
这个方案完全符合Swift Observation框架的设计哲学,既解决了性能问题,又保持了代码的简洁性,而且是Apple主推的未来方向。你可以直接把这段代码套进去,应该就能完美解决你的问题啦!
内容来源于stack exchange

