SwiftData 的新特性
What's new in SwiftData
2024年6月10日
一句话判断
SwiftData 今年补上了三个关键短板:唯一约束、自定义数据存储、复杂查询优化,从”能用的 Core Data 替代品”变成了”值得认真投入的数据层方案”。
这场 Session 讲了什么
SwiftData 在 iOS 17 发布时主打”用宏搞定一切”的简洁体验,但实际使用中很快会遇到数据去重、存储格式定制、查询性能这些绕不过去的问题。今年的更新直接回应了这些痛点。
新增的 #Unique 宏让 SwiftData 具备了 upsert 能力——当多个 Model 实例在指定属性组合上重复时,自动执行更新而非插入。自定义数据存储(Custom Data Store)允许你替换底层的 SQLite 实现为任意格式(比如 JSON 文件),同时保留 @Model、@Query 等所有上层 API 不变。查询方面新增了复杂的 #Predicate 构建能力和 FetchDescriptor 的性能优化,还有新的 @Query 宏变体可以更精细地控制查询行为。
值得深挖的点
#Unique 宏:不只是去重,是身份定义
#Unique 的设计思路比单纯的”唯一索引”更深层。它解决的不仅是数据重复的问题,而是在定义 Model 的”业务身份”。在示例应用 Trips 中,一个 Trip 的唯一性由 (name, startDate, endDate) 三个属性共同决定——同名但不同日期的旅行是合法的。
当 SwiftData 检测到唯一约束冲突时,执行的是 upsert 操作:用新值更新已有记录,而非插入新记录或抛出异常。这对数据同步场景非常关键。配合 preserveValueOnDeletion 属性标记和 SwiftData History API,你可以追踪被删除记录的”墓碑”信息,知道哪条记录被替换了、替换前是什么值。
这个设计的 trade-off 在于:唯一约束只能在编译时通过宏定义,不支持运行时动态添加。如果你需要在用户使用过程中动态决定哪些字段组合需要唯一,只能自己在应用层处理。
自定义数据存储:解耦 API 和持久化
这是今年最大的架构变化。之前 SwiftData 绑死了默认的 SQLite 存储后端,现在你可以实现自己的 DataStore 协议,用任何格式来持久化数据——JSON 文件、远程 API、甚至是内存数据库。示例中的 JSONStoreConfiguration 展示了如何用 JSON 文件作为存储后端。
这个设计最大的好处是渐进式采纳:你的自定义存储不需要一次性实现所有 SwiftData 特性。基础的增删改查可以先跑起来,复杂的特性(如同步、迁移)可以后续逐步支持。上层代码完全不需要改动,@Query 和 @Model 照常使用。
但要注意,自定义存储的性能天花板取决于你自己的实现质量。SwiftData 默认的 SQLite 存储经过深度优化,如果只是简单场景(比如导出为 JSON 格式文档),自定义存储是合理的;如果追求查询性能,默认存储仍然是更好的选择。
代码片段
使用 #Unique 宏定义复合唯一约束
@Model
class Trip {
#Unique<\Trip>([\.name, \.startDate, \.endDate])
var name: String
var startDate: Date
var endDate: Date
@Attribute(.preserveValueOnDeletion)
var name: String // 删除后保留该值用于历史追踪
init(name: String, startDate: Date, endDate: Date) {
self.name = name
self.startDate = startDate
self.endDate = endDate
}
}
场景:需要确保同名同时段的旅行不会重复创建。坑:#Unique 中引用属性必须用 KeyPath 语法 \.,用字符串会编译报错。
配置自定义数据存储
// 替换默认的 SQLite 存储为自定义 JSON 存储
let container = try ModelContainer(
for: Trip.self,
configurations: [JSONStoreConfiguration(url: jsonFileURL)]
)
// 上层代码完全不受影响,@Query 照常工作
@Query var trips: [Trip]
场景:App 的数据需要以 JSON 文件形式导出或导入。坑:自定义存储需要你自己处理并发安全和数据一致性,默认存储帮你做了这些。
用 PreviewModifier 构建 SwiftData 预览
struct SampleData: PreviewModifier {
static func makeSharedContext() async throws -> ModelContainer {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try ModelContainer(for: Trip.self, configurations: config)
// 插入示例数据,#Unique 会自动处理重复
let context = container.mainContext
SampleData.insertSampleTrips(into: context)
return container
}
static func place(into context: ModelContainer, body: Content) -> some View {
body.modelContainer(context)
}
}
// 在 Preview 中使用
#Preview(traits: .sampleData) {
TripListView()
}
场景:为 SwiftUI 预览提供稳定的示例数据。坑:预览用的内存容器不会自动清理,每次修改 Preview 代码会重新执行 makeSharedContext。
最佳实践
新项目: 直接用 SwiftData 起步,#Unique 宏在项目初期就加上,定义清楚每个 Model 的业务身份。自定义存储只在有明确需求(如文档格式兼容)时才考虑,默认存储已经够用。
已有项目: 如果已经在用 Core Data,迁移到 SwiftData 的优先级取决于你是否需要 #Unique 和更简洁的查询语法。SwiftData 和 Core Data 共享同一个底层存储格式,可以渐进迁移。如果你的 Core Data 项目已经有成熟的唯一约束和查询方案,不需要急着全量切换——但新增的 Model 可以直接用 SwiftData。
还有什么值得关注
ModelConfiguration现在支持更细粒度的配置,包括自定义存储路径和独立的存储配置。- Xcode Preview 对 SwiftData 的支持大幅改善,
@Previewable宏可以直接在 Preview 中使用 SwiftData Model。 - SwiftData History API 配合
preserveValueOnDeletion可以追踪数据变更历史,适合需要审计或同步的场景。