
引子写 SwiftData 久了大概都会碰到同一个别扭的地方。Query很好用——挂在 View 上列表自动刷新过滤排序也都顺手。可一旦逻辑稍微复杂一点比如要根据所有行程算地图镜头范围或者要在一个Observable的 Store 里汇总预算你就会发现Query跟不过去了。它只能待在 SwiftUI 视图里。以前怎么办自己听ModelContext通知或者干脆把聚合计算塞进 View。能跑但总觉得哪里不对业务明明不该长在界面上。WWDC26 的 SwiftData 专场里Apple 给这个问题塞了个答案还盖了个绿色的 NEW 标签ResultsObserver。让我们一起来了解一下吧它到底解决什么问题官方说法很直白在某个ModelContext里按你给定的条件拉一批模型然后持续盯着这批结果数据变了结果跟着变。听起来像Query对能力上确实很像——过滤、排序、分区都支持。关键差别只有一个Query是 View 的属性ResultsObserver是你自己拿着的对象。ViewModel、后台协调器甚至 SceneKit 那种根本不沾 SwiftUI 的代码都能用。Apple 自己举的例子就是行程变了地图相机边界要重算——这件事显然更适合放在一个 Controller 里而不是某个List的body里。类型大概长这样finalclassResultsObserverElement,SectionNamewhereElement:PersistentModel,SectionName:Hashable不需要分区的时候第二个类型参数传Never就行意思就是「别分组给我平铺的结果」letobservertryResultsObserverTrip,Never(modelContext:modelContext)读observer.results就是当前命中的那批模型。怎么用一个控制器 一条观察线WWDC 里的写法大致如下。注意两件事一是withContinuousObservation二是返回的那个token。importSwiftDataimportObservationimportMapKitObservableMainActorfinalclassMapCameraController{privateletresultsObserver:ResultsObserverTrip,Nevervarbounds:MapCameraBounds?ObservationIgnoredprivatevartoken:ObservationTracking.Token?init(modelContext:ModelContext)throws{resultsObservertryResultsObserverTrip,Never(modelContext:modelContext)tokenwithContinuousObservation(options:[.didSet]){[weakself]_inguardletselfelse{return}boundscalculateBounds(trips:resultsObserver.results)}}privatefunccalculateBounds(trips:[Trip])-MapCameraBounds?{// 根据 trips 算镜头范围nil}}流程其实不玄先建一个ResultsObserver盯住 store再用withContinuousObservation(options: [.didSet])注册回调结果集变了就重算最后把返回的ObservationTracking.Token存起来。Token 这东西容易被忽略。它相当于「这条监听还活着」的凭证——属性没了观察也就停了。所以别建完就扔挂在实例上。还有个细节闭包里一定要读到resultsObserver.results。Swift Observation 靠「你访问了什么」决定「下次通知谁」。你不读results它就不知道你在乎这个集合。再挖一点withContinuousObservation在干什么这不是 SwiftData 私有的魔法是 Observation 框架里偏「长期监听」的那一挂。以前的withObservationTracking更像单次跟踪属性要变时喊一声。withContinuousObservation则是持续回调所以才需要外部持有 Token不再持有或者主动cancel监听就结束。[.didSet]的意思是值已经写完再通知你。适合做汇总、重算边界这类「根据最新快照更新派生状态」的事。如果在MainActor上注册回调也会回到主线程写 UI 状态会省心不少。可以简单记Observer 负责数据源Continuous Observation 负责把变化递给你Token 负责线路不断。更贴近日常的例子预算汇总地图镜头有点「演示味」。换个更常见的预算 App 要显示总额度、已花、剩余还有哪些超支了。这些数字依赖一整批Budget既不适合塞进单个模型也不适合永远写在 View 里——尤其你还想单测的时候。ObservablefinalclassBudgetSummaryStore{privateletobserver:ResultsObserverBudget,NeverObservationIgnoredprivatevartoken:ObservationTracking.Token?vartotalBudget:Double0vartotalSpent:Double0varremaining:Double0varoverspentBudgets:[Budget][]init(modelContext:ModelContext)throws{observertryResultsObserverBudget,Never(modelContext:modelContext)tokenwithContinuousObservation(options:[.didSet]){[weakself]_inself?.updateSummary()}}privatefuncupdateSummary(){letbudgetsobserver.results totalBudgetbudgets.reduce(0){$0$1.limit}totalSpentbudgets.reduce(0){sum,budgetinsumspentAmount(for:budget)}remainingtotalBudget-totalSpent overspentBudgetsbudgets.filter{spentAmount(for:$0)$0.limit}}privatefuncspentAmount(forbudget:Budget)-Double{budget.expenses.reduce(0){$0($1.amount*Double($1.quantity))}}}View 只负责展示structBudgetDashboardView:View{Environment(BudgetSummaryStore.self)privatevarstorevarbody:someView{VStack(alignment:.leading,spacing:8){Text(总额度\(store.totalBudget,format:.currency(code:CNY)))Text(已花费\(store.totalSpent,format:.currency(code:CNY)))Text(剩余\(store.remaining,format:.currency(code:CNY)))}}}单测也好办内存版ModelContainer插入几条数据直接断言 Store 里的数字。不必为了测业务逻辑去拉起一整棵 SwiftUI。过滤、排序也行不是只能全表盯着需要条件时塞FetchDescriptor即可letdescriptorFetchDescriptorTrip(predicate:#Predicate{$0.endDateDate.now},sortBy:[SortDescriptor(\.startDate)])letupcomingtryResultsObserverTrip,Never(fetchDescriptor:descriptor,modelContext:modelContext)要按字段分区用带sectionBy的初始化并给SectionName传具体类型。之后可以拿sections也有element(at:)、indexPath(for:)这类接口和现在强化过的 sectionedQuery是同一套思路。有个现实限制目前分区的 key path 更偏向字符串一类场景。要是你的分区键更复杂可能得拆多个 Observer或者给 Apple 提 Feedback。别跟 HistoryObserver 搞混同一场 WWDC 还带了个HistoryObserver。名字都带 Observer用途却不一样。ResultsObserver关心的是现在这批结果长什么样。适合汇总、派生状态、驱动非 SwiftUI 的展示。HistoryObserver关心的是发生了哪些变更事务。它暴露一个会递增的eventCounter适合你跟着去fetchHistory做同步、审计、推后端。一个看快照一个看脚印。地图跟着行程动用 Results本地改动要推到自建服务器用 History。什么时候该用什么时候别用说白了就三条界面上就是展示列表、详情——继续Query别为了新而新。逻辑依赖一批模型、又不该长在 View 上或者根本不在 SwiftUI 里——上ResultsObserver。你要的是变更流水而不是当前集合——看HistoryObserver。另外社区里有个提醒我觉得挺对别因为有了 ResultsObserver就把所有逻辑都搬进Observable类。展示逻辑可以留在 View单模型规则留在 Model只有那些「跨一批模型、又不属于界面」的东西才值得单独拎出来。收个尾SwiftData 早期最让人难受的地方之一就是观察能力几乎绑死在 SwiftUI 上。业务要么挤进 View要么自己接通知。ResultsObserver没发明全新范式只是把大家早就想要的能力做成了框架原生 API查询结果变成可持有的 Observable 对象过滤排序分区还在观察走的是 Swift Observation。对写列表的人来说可能感知不强对开始拆 Store、写单测、做非 SwiftUI 消费端的人来说这缺口终于补上了。参考WWDC26 Session 274、ResultsObserver 文档。