SwiftData 的新功能
What’s new in SwiftData
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:SwiftData 终于把数据监听能力从 SwiftUI 视图里剥离出来了,ModelResultsObserver 让你能在纯 ViewModel 或后台服务里优雅地响应数据库变化,不用再写恶心的 workaround。
这场 Session 讲了什么
过去两年用 SwiftData 的人都有个痛点:@Query 死死绑在 SwiftUI 视图上。如果你想在非 UI 层(比如一个负责计算地图边界的 Controller,或者一个后台同步服务)监听数据变化,只能自己手写一堆 NotificationCenter 或者硬凑一个隐藏的 SwiftUI 视图。Apple 这次算是向现实妥协了,推出了 ModelResultsObserver 和 HistoryObserver,把查询和监听能力下放给了普通的 Swift 代码。
除了架构层面的解绑,这次还补了几个实用的语法糖。@Query 终于原生支持 sectionBy 分组了,写带 Section 的 List 不用再自己手写 Dictionary 分组逻辑。针对 MapKit 这种第三方库里的复杂类型,新增了 @Attribute(.codable) 作为逃生舱,直接序列化存库,省去了写 Transformable 的麻烦。
这对采用 MVVM 架构或者在 App 里混合使用 UIKit/SwiftUI 的团队影响最大。你终于可以把数据获取逻辑从 View 层抽离到 ViewModel 里,让 View 只做纯粹的渲染。
值得深挖的点
ModelResultsObserver 与 withContinuousObservation 的化学反应
Apple 之前强推 “View 直接持有 Query” 的模式,导致很多复杂的业务逻辑被迫塞进 View 的 body 或者 View 的初始化方法里。ModelResultsObserver 的出现标志着 Apple 承认了独立状态对象(State Object)的必要性。它支持过滤、排序甚至分区,API 设计和 @Query 几乎一致。
配合 Swift 的 Observation 框架,你可以通过 withContinuousObservation(options: [.didSet]) 拿到一个 token,在闭包里响应数据变化。这里的设计 trade-off 很明显:Apple 没有搞回调代理(Delegate)或者 Combine Publisher,而是完全拥抱了 Swift 6 时代的 Observation 宏。坑在于生命周期管理,withContinuousObservation 返回的 token 必须被强引用(比如存为类的属性),一旦 token 被释放,监听就停了。这跟以前用 AnyCancellable 或者 Task 的逻辑一样,新手很容易因为没存 token 导致监听失效。
@Attribute(.codable) 是逃生舱,不是银弹
遇到 MKMapItem.Identifier 这种不属于你、又没标记 @Model 的第三方类,以前要么转成基础类型,要么用老旧的 Transformable。现在直接加个 @Attribute(.codable) 就能存,代码极其干净。
但 Apple 在 Session 里特意强调了它的局限性,这点非常实在。Codable 属性在数据库里就是一坨不透明的二进制(或 JSON)数据,SwiftData 根本不知道里面有什么。这意味着你绝对不能在 #Predicate 里用它做过滤,也不能用它来排序。更致命的是,如果这个第三方类的 Codable 结构升级了(比如加了个非 Optional 属性),SwiftData 的自动迁移(Migration)是感知不到的,直接读取就会 Crash。所以,只把它当成处理外部类型的 “逃生舱”(escape hatch),你自己写的业务模型,老老实实用 @Model 或者基础值类型。
代码片段
1. @Query 原生分组
场景:在 SwiftUI 列表中按目的地对行程进行 Section 分组。
struct TripListView: View {
// 使用 sectionBy 参数按 destination 属性分组
@Query(sort: \Trip.startDate, sectionBy: \.destination)
var trips: [Trip]
var body: some View {
List {
// 通过下划线前缀访问底层的 Query 对象,获取 sections
ForEach(_trips.sections) { section in
Section(section.id) {
// section 本身是一个集合,直接遍历即可
ForEach(section) { trip in
TripListItem(trip: trip)
}
}
}
}
}
}
坑:section.id 的类型取决于你传入的 KeyPath 的值类型,如果是 Optional 类型,UI 渲染时可能会遇到 nil 的排版问题,建议确保分组键是非空字符串。
2. 使用 @Attribute(.codable) 存储第三方类型
场景:在行程模型中直接持久化 MapKit 的 MKMapItem.Identifier。
@Model
class Trip {
var name: String
var destination: String
// 第三方类型无法标记 @Model,使用 .codable 作为逃生舱
@Attribute(.codable)
var mapItemIdentifier: MKMapItem.Identifier?
}
坑:千万别在 #Predicate 里写 \Trip.mapItemIdentifier == someId,编译能过但运行时会直接抛出异常,因为 SwiftData 无法解析 Codable 数据的内部结构。
3. 在非 UI 层监听数据变化
场景:在独立的 Controller 中监听行程变化,动态重新计算地图的相机边界。
@Observable
@MainActor
final class MapCameraController {
private let observer: ModelResultsObserver<Trip>
var bounds: MapCameraBounds?
// 必须强引用 token,否则监听会立即失效
private var token: ObservationTracking.Token?
init(modelContext: ModelContext) throws {
observer = try ModelResultsObserver<Trip>(modelContext: modelContext)
// 使用 didSet 选项,在数据发生变化时触发闭包
token = withContinuousObservation(options: [.didSet]) { [weak self] event in
guard let self else { return }
// 重新计算边界
self.bounds = self.calculateBounds(trips: self.observer.results)
}
}
}
坑:withContinuousObservation 闭包内必须实际读取 observer.results,Swift 的 Observation 框架是通过追踪属性访问来建立依赖的,不读取就不会触发更新。
最佳实践
- 已有项目的迁移策略:如果你之前为了在非 UI 层监听 SwiftData 数据,写了基于
NSManagedObjectContextDidSave的 Notification 监听,或者搞了个隐藏的 SwiftUI View 来桥接@Query,现在可以全部删掉,替换为ModelResultsObserver。这能让你的 ViewModel 或 Controller 代码干净一半。 - 新项目的采用建议:大胆把数据获取逻辑从 View 层抽离。View 层的
@Query只用于最简单的列表渲染;涉及复杂计算、网络请求联动、地图边界计算的场景,统统交给持有ModelResultsObserver的@Observable状态对象。 - 实战中容易踩的坑:使用
HistoryObserver做云端同步时,一定要在初始化时传入authors参数(比如["App"])。如果不加过滤,你会把从服务器拉取下来的数据变更再次触发同步,直接导致死循环。
还有什么值得关注
- HistoryObserver 的 eventCounter:Apple 没有给历史记录搞复杂的回调,而是暴露了一个简单的递增计数器
eventCounter,配合fetchHistory()使用,这种 “拉” 而不是 “推” 的设计大大降低了状态同步的复杂度。 - ObservationTracking.Token 的生命周期:这次 Session 反复强调要把 token 存起来,这暗示了 Swift Observation 框架底层的监听机制是弱引用的,忘记存 token 将是未来一年 Swift 论坛里最常见的 Bug 提问。
- Section 的底层实现:
_trips.sections返回的集合直接实现了RandomAccessCollection,这意味着在 SwiftUI 里嵌套ForEach时,性能和自己手写 Dictionary 分组没有区别,Apple 在底层做了优化。