SwiftData 在 2027 版中扩展了三个关键方向:通过 Codable 持久化自定义及第三方类型、为 SwiftUI 列表组织分区数据,以及通过 ResultsObserver 和 HistoryObserver 观察数据存储变化。这些能力让模型不必被拆成大量基础字段,也让后台同步、审计和跨界面刷新有了更明确的入口。
Codable 让领域模型少做一层转换
过去遇到坐标、货币、第三方 SDK 值对象等类型时,常见做法是将其拆成若干 SwiftData 字段,或者额外维护序列化数据。新版本允许通过 Codable 持久化这类值,模型可以更贴近业务语义。
下面是一个可以改造进项目的最小示例。示例假设项目使用支持该能力的 2027 SDK;具体属性宏或约束名称应以对应测试版 SDK 文档为准。
import Foundation
import SwiftData
struct Money: Codable, Equatable, Sendable {
var amountInCents: Int
var currencyCode: String
}
@Model
final class Invoice {
var number: String
var customer: String
var issuedAt: Date
var total: Money
init(number: String, customer: String, issuedAt: Date, total: Money) {
self.number = number
self.customer = customer
self.issuedAt = issuedAt
self.total = total
}
}
@MainActor
func insertExampleInvoice(into context: ModelContext) throws {
let invoice = Invoice(
number: "INV-2027-001",
customer: "Example Studio",
issuedAt: .now,
total: Money(amountInCents: 129_00, currencyCode: "CNY")
)
context.insert(invoice)
try context.save()
}
这类设计尤其适合具有稳定编码格式的值对象。不过,Codable 不是免费的模式迁移机制。删除字段、改变枚举编码值或调整第三方类型的编码实现,都可能让旧数据无法解码。生产项目应固定编码结构,并为升级路径准备版本字段或自定义 init(from:)。
第三方类型还存在另一个边界:如果类型的 Codable 实现由依赖库控制,升级依赖就可能改变持久化格式。更稳妥的方式通常是定义自己的存储 DTO:
import Foundation
struct StoredCoordinate: Codable, Equatable {
let latitude: Double
let longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
这样既能使用 Codable 持久化,又不会把数据库兼容性交给第三方包的发布节奏。
查询结果直接服务于 SwiftUI 分区列表
新版本还支持把数据组织成 SwiftUI 列表分区。价值不只是少写几行 Dictionary(grouping:):分区逻辑进入查询层后,排序、刷新和界面结构可以共享同一套规则。
以发票列表为例,可以按月份展示分区。由于来源摘要没有给出分区查询 API 的完整签名,下面使用当前 SwiftUI 可直接运行的分组方式展示界面结构;迁移到新 SDK 时,可以把 sections 替换为 SwiftData 提供的分区查询结果。
import SwiftUI
import SwiftData
struct InvoiceList: View {
@Query(sort: \Invoice.issuedAt, order: .reverse)
private var invoices: [Invoice]
private var sections: [(month: Date, invoices: [Invoice])] {
let calendar = Calendar.current
let grouped = Dictionary(grouping: invoices) { invoice in
let components = calendar.dateComponents(
[.year, .month],
from: invoice.issuedAt
)
return calendar.date(from: components)!
}
return grouped
.map { (month: $0.key, invoices: $0.value) }
.sorted { $0.month > $1.month }
}
var body: some View {
List {
ForEach(sections, id: \.month) { section in
Section(section.month.formatted(.dateTime.year().month())) {
ForEach(section.invoices) { invoice in
VStack(alignment: .leading) {
Text(invoice.number)
Text(invoice.customer)
.foregroundStyle(.secondary)
}
}
}
}
}
}
}
手工分组适合验证界面,但数据量变大后会在每次视图更新时重新遍历结果。原生分区查询更值得用于长期实现,尤其是在分区键、排序和谓词能够由存储层处理时。需要特别检查的是:分区键是否稳定、分区内排序是否确定,以及空分区在删除最后一条记录后如何消失。
ResultsObserver 与 HistoryObserver 解决不同层次的问题
ResultsObserver 和 HistoryObserver 都用于观察变化,但它们面向的需求并不相同。
ResultsObserver更接近查询结果:适合关心某个结果集合是否增加、删除或更新,例如徽标计数、派生缓存和非 SwiftUI 控制器。HistoryObserver更接近存储历史:适合增量同步、审计记录、索引更新,以及处理其他进程或上下文写入的数据。
可以这样划分职责:界面继续优先使用 @Query 或分区查询;需要脱离视图生命周期观察具体集合时使用 ResultsObserver;需要消费可追踪的持久化变更时使用 HistoryObserver。
以下是观察器接入时可以采用的伪项目骨架。它展示生命周期和处理边界,不承诺具体初始化参数;请按 2027 SDK 中两个观察器的实际签名替换标记位置。
import SwiftData
@MainActor
final class StoreChangeCoordinator {
private var resultsObserver: ResultsObserver?
private var historyObserver: HistoryObserver?
func start(container: ModelContainer) {
// 按 2027 SDK 的实际 API 构造观察器,并保存强引用。
// resultsObserver = ResultsObserver(...)
// historyObserver = HistoryObserver(...)
// resultsObserver?.start()
// historyObserver?.start()
}
func stop() {
// 按 SDK 要求取消订阅,避免重复回调或延长上下文生命周期。
// resultsObserver?.stop()
// historyObserver?.stop()
resultsObserver = nil
historyObserver = nil
}
}
观察回调中不要直接执行长时间网络请求。更可靠的流程是读取变化标识、写入本地任务队列,再由独立 worker 执行同步。还应保存 history token 或等价游标,确保应用重启后能够从上次位置继续,而不是重复扫描全部历史。
升级时应先验证的清单
采用这些能力时,建议把迁移拆成三个独立步骤,而不是一次同时替换模型、查询和同步链路。
- 先为 Codable 类型建立往返测试:写入旧版本样本,升级后读取,再验证重新编码的数据仍兼容。
- 将一个低风险列表迁移到分区查询,测量首屏时间、滚动性能和批量变更时的刷新行为。
- 为观察器增加幂等处理、取消订阅和断点续传测试,尤其关注应用重启及多上下文写入。
- 不要在持久化模型中直接依赖编码格式不稳定的第三方类型;使用自有 DTO 隔离变化。
- 确认观察器的线程或 actor 约束,避免在回调中跨上下文传递 SwiftData 模型实例。
这次升级的重点不是增加更多模型语法,而是让“值如何保存、结果如何展示、变化如何传播”形成更完整的链路。Codable 持久化能减少样板转换,分区查询能降低界面整理成本,观察器则补上后台工作流所需的变化信号。真正落地时,数据格式兼容性和观察任务的幂等性仍然需要应用自己负责。