Highlight
SwiftData 新增三项能力:
@Query支持按属性分组(sectionBy),@Attribute(.codable)可持久化第三方框架类型,ModelResultsObserver和HistoryObserver让非 SwiftUI 代码也能监听数据变化。
核心内容
列表分组不再需要手写字典
做过分组列表的开发者都经历过这个流程:先从 SwiftData 查出全部数据,再按某个属性手动分组成 Dictionary,最后把字典转成 Section 塞进 List。代码不短,而且每次数据变化都要重新算一遍分组。
(00:36)Apple 在 @Query 上加了 sectionBy 参数。传一个 KeyPath 进去,SwiftData 直接在查询层完成分组。视图层只需要两层 ForEach:外层遍历 sections,内层遍历每个 section 里的数据。
第三方类型终于有了逃生舱
想在模型里存一个 MKMapItem.Identifier?以前 SwiftData 会直接抛 fatal error:“Class property within Persisted Struct/Enum is not supported”。因为 MKMapItem.Identifier 是 MapKit 的 class,你没源码给它加 @Model,SwiftData 也 inspect 不了它。
(04:52)新加的 @Attribute(.codable) 让 SwiftData 把序列化委托给类型自己。只要类型遵循 Codable,SwiftData 就存它的编码结果,不再尝试生成 schema。
代价是有的:Codable 属性对 SwiftData 不透明,不能用于 Predicate 过滤,也不能排序。类型结构变了也不会触发迁移。Apple 明确说这是一个 “escape hatch”,只用于你不控制的第三方类型,自己写的模型还是应该用 @Model 或值类型。
数据监听走出 SwiftUI
@Query 只能在 SwiftUI 视图里用。如果你的 app 有一个独立的状态对象需要从 SwiftData 取数据并响应变化,以前只能写 workaround:藏一个看不见的 SwiftUI 视图,或者自己接 NotificationCenter。
(07:21)ModelResultsObserver 把 @Query 的能力拆了出来。它支持过滤、排序、分组,配合 Swift Observation 框架的 withContinuousObservation,任何 Swift 代码都能收到数据变化的通知。
(09:30)HistoryObserver 走得更远。SwiftData 每次保存都会记录历史事务(history transaction),包含改了什么、谁改的、以及一个唯一 token。HistoryObserver 监听这些事务,适合用来做服务端同步或者响应 app extension 的修改。你可以按作者过滤,避免把服务端拉下来的改动又推回去。
详细内容
Sectioned Fetching
(00:01)@Query 的 sectionBy 参数接收一个 KeyPath,按该属性的值把查询结果分组。 wrapped value 仍然是 [Trip],但可以通过下划线前缀访问底层的 Query 对象,拿到 sections 集合。
struct TripListView: View {
@Query(sort: \Trip.startDate, sectionBy: \.destination)
var trips: [Trip]
var body: some View {
List(selection: $selection) {
ForEach(_trips.sections) { section in
Section(section.id) {
ForEach(section) { trip in
TripListItem(trip: trip)
}
}
}
}
}
}
关键点:
sectionBy: \.destination告诉 SwiftData 按destination属性分组_trips是属性包装器的底层实例,通过它访问sections- 每个
section的id就是分组键的值,这里用作Section的 header - 内层
ForEach遍历的是section本身,它是一个RandomAccessCollection
Codable 属性
(04:59)@Attribute(.codable) 把持久化交给类型的 Codable 实现。SwiftData 不再为这类属性生成 schema,而是存编码后的数据。
import SwiftData
@Model class Trip {
struct Location: Codable {
var latitude: Double
var longitude: Double
}
var name: String
var destination: String
var startDate: Date
var endDate: Date
var location: Location?
@Attribute(.codable) var mapItemIdentifier: MKMapItem.Identifier?
}
关键点:
MKMapItem.Identifier是 MapKit 的 class,SwiftData 无法自动 inspect- 它遵循
Codable,所以可以用.codable属性标记 - SwiftData 存的是编码后的二进制/JSON,内部结构对它不透明
- 不能用于
#Predicate过滤或SortDescriptor排序 - 类型结构变更不会触发自动迁移,需要保证前后兼容的编码逻辑
ModelResultsObserver
(08:20)ModelResultsObserver 在非 SwiftUI 代码中提供与 @Query 等价的能力:查询数据并持续监听变化。
@Observable @MainActor final class MapCameraController {
private let observer: ModelResultsObserver<Trip>
var bounds: MapCameraBounds?
private var token: ObservationTracking.Token?
init(modelContext: ModelContext) throws {
observer = try ModelResultsObserver<Trip>(modelContext: modelContext)
token = withContinuousObservation(options: [.didSet]) { [weak self] _ in
self?.bounds = self?.calculateBounds(trips: observer.results)
}
}
private func calculateBounds(trips: [Trip]) -> MapCameraBounds? { /* ... */ }
}
关键点:
ModelResultsObserver初始化时需要ModelContext- 支持
predicate、sort和sectionBy,API 与@Query一致 withContinuousObservation返回的token必须被强引用,否则监听立即失效- 闭包内需要读取
observer.results,Observation 框架通过属性访问追踪依赖 [weak self]避免循环引用
HistoryObserver
(08:21)HistoryObserver 监听 SwiftData 的持久化历史事务,适合服务端同步场景。
@SyncActor final class ServerSync {
private let observer: HistoryObserver
private var token: ObservationTracking.Token?
func start() throws {
observer = try HistoryObserver(authors: ["App"], modelContainer: modelContainer)
token = withContinuousObservation(options: .didSet) { [weak self] _ in
_ = self?.observer.eventCounter
self?.processChanges()
}
}
private func processChanges() {
// 用 ModelContext.fetchHistory 获取事务,上传到服务器
}
}
关键点:
authors参数过滤事务来源,避免把服务端同步下来的改动又推回去eventCounter是唯一的可观察属性,新事务到来时递增- 闭包内必须访问
eventCounter,Observation 框架才能建立依赖关系 - 实际处理时调用
ModelContext.fetchHistory获取事务详情 - 每个事务有唯一 token,可用于追踪已处理的变更
核心启发
1. 把数据层从视图层彻底抽离
以前 MVVM 架构下,ViewModel 想监听 SwiftData 变化只能绕路。现在用 ModelResultsObserver,ViewModel 自己持有数据监听,View 只负责渲染。做什么:重构现有项目,把 @Query 从 ViewModel 里移出去。为什么值得做:View 和数据的耦合降低,测试更容易。怎么开始:把 @Query 换成 ModelResultsObserver,用 withContinuousObservation 接收变化后通过 @Observable 通知 View。
2. 做一个实时协作的笔记 App
HistoryObserver 的 authors 过滤和事务 token 天然适合多端同步。做什么:本地编辑产生的事务标记为 "App",服务端推送的事务标记为 "Server",HistoryObserver 只监听 "App" 的变更并上传。为什么值得做:不需要自己实现变更追踪,SwiftData 已经帮你记好了。怎么开始:初始化 HistoryObserver 时传入 authors: ["App"],在 processChanges 里调用 fetchHistory 获取事务列表。
3. 在 SpriteKit/SceneKit 游戏里用 SwiftData 存进度
游戏通常不用 SwiftUI,以前 SwiftData 的实时监听能力用不上。做什么:用 ModelResultsObserver 监听玩家数据变化,动态调整游戏状态。为什么值得做:游戏架构不用为了用 SwiftData 而硬塞 SwiftUI。怎么开始:在游戏的 GameManager 里持有一个 ModelResultsObserver<PlayerData>,数据变化时更新游戏内变量。
4. 按日期分组的日程/待办列表
sectionBy 让按日期分组变得 trivial。做什么:日程 App 里按天分组显示事件。为什么值得做:以前要手动按 Calendar 分组,现在一行 sectionBy 搞定。怎么开始:@Query(sort: \.startDate, sectionBy: \.dateString),其中 dateString 是一个计算属性返回格式化后的日期字符串。
5. 在模型里直接存复杂配置对象
有些第三方 SDK 的配置类型遵循 Codable 但结构很复杂。做什么:用 @Attribute(.codable) 直接存进 SwiftData,不用手动拆成基础类型。为什么值得做:省掉一堆转换代码,模型更干净。怎么开始:确认类型遵循 Codable,加 @Attribute(.codable) 标记即可。
关联 Session
- What’s new in SwiftUI — SwiftUI 的新功能与 SwiftData 的
@Query深度集成 - What’s new in SwiftData persistence — SwiftData 持久化层的底层改进
- Track Model Changes with SwiftData History — 持久化历史事务的详细机制(WWDC 2024)
- Add persistence with SwiftData — SwiftData 入门 code-along
- What’s new in Swift — Swift 语言新特性,包括 Observation 框架的改进
评论
GitHub Issues · utterances