SwiftData 真正的成本在于 Schema 纪律
Get Bananas 的 ShoppingItem 是说明 SwiftData Schema 纪律为何重要的典型例子。最初的 Schema 并不包含 lastModified 时间戳;后来补上它时,由于磁盘上已经存在数据,必须采用特定形态的迁移。而这个字段之所以被定义为可选,正是为了修复它最初以非可选形式加入时触发的迁移崩溃。1
SwiftData 的 API 就是两个宏。在类上加 @Model,它就成了持久化类型;在属性上加 @Attribute(.unique),它就获得唯一性约束。框架隐藏了 Core Data 的栈管理、值转换器的繁琐步骤以及 NSManagedObjectContext 的样板代码。框架没有隐藏的,是 Schema 迁移——它只是把迁移从命令式变成了声明式。不认真对待迁移的代价,就是一次例行更新抹掉用户数据的那种 bug。
本文的观点是:SwiftData 起步很便宜,草率迁移却很昂贵。所谓纪律,就是从第一天起就管好命名、可选性和 VersionedSchema,而不是等到意识到早该如此的那天才动手。
TL;DR
@Model宏把一个类变成可持久化的 SwiftData 类型。框架在编译期依据属性声明生成 Schema。- 新增一个可选属性等于零成本迁移:SwiftData 的轻量迁移会自动处理。而向既有 Schema 添加非可选属性,则需要
VersionedSchema加上一份MigrationPlan,告诉框架如何为已有的行填充这个新字段。 - 第一天就跳过
VersionedSchema的代价是:v2 里任何一次不平凡的 Schema 变更都可能危及用户的数据库——轻量路径很保守,一旦无法推断出迁移方式就会直接放弃。 @Attribute(.unique)适用于自然键(您自己生成的UUID、从外部导入的 ID)。@Relationship适用于父子引用。两者都是宏,会在底层生成相应的 Core Data 管道代码。2
@Model 究竟做了什么
SwiftData 类型就是一个应用了 @Model 宏的 Swift 类。Get Bananas 的 ShoppingItem 是最典型的形态:
import Foundation
import SwiftData
@Model
final class ShoppingItem {
@Attribute(.unique) var id: UUID
var name: String
var amount: String
var section: String
var isChecked: Bool
var isOptional: Bool
var sortOrder: Int
var lastModified: Date?
init(id: UUID = UUID(), name: String, amount: String, section: String,
isOptional: Bool = false, sortOrder: Int = 0) {
self.id = id
self.name = name
self.amount = amount
self.section = section
self.isChecked = false
self.isOptional = isOptional
self.sortOrder = sortOrder
self.lastModified = Date()
}
}
关于这种形态,API 隐藏了三个细节。
@Model 不需要另行声明持久化存储的 Schema。 SwiftData 在编译期读取类定义并合成 Schema:类的各个属性成为模型的字段,它们的 Swift 类型成为对应列的类型。没有 .xcdatamodeld 文件需要维护(尽管 Core Data 底层的 NSManagedObjectModel 依然存在,运行时正是它在支撑这份 Schema)。2
@Attribute(.unique) 是对单个列的约束,而不是 PRIMARY KEY 声明。 SwiftData 的持久化标识是 PersistentIdentifier,由框架为每一行自动生成。@Attribute(.unique) 告诉框架:“这一列的每个取值最多对应一行。”当您插入的模型带有一个已经存在的 .unique 值时,SwiftData 会执行 upsert——更新已有的那一行,而不是拒绝写入。这个语义对产品代码很关键:.unique 不是阻止用户提交重复项的 UI 层校验,而是一种“至多一行”的存储保证,它会悄悄合并数据。上面的 id: UUID 模式正是跨进程同步的推荐做法(此时您需要一个稳定的标识符,即便进程内的 PersistentIdentifier 消失也依然有效);而当同一个 UUID 从两条同步路径抵达时,upsert 行为恰恰是您想要的。
@Model 类是引用类型,不是值类型。 修改 ShoppingItem 实例的某个属性会触发 SwiftData 的变更追踪:框架记录这次变更,并在下一次上下文保存时写入磁盘。通过 @Query 实现的 SwiftUI 集成,会重新渲染所有观察匹配谓词的视图。这套机制与 @Observable 类似(详见SwiftUI 由什么构成),只是在其上又叠加了一层持久化。
可选字段是廉价的迁移
ShoppingItem 上的 lastModified: Date? 是可选的,而这份可选性是承重的。该字段在 v1 上线之后才加入,用于支持跨设备同步与冲突解决;用户设备上已有的行并没有 lastModified 值。一个没有默认值的可选字段,让 SwiftData 的轻量迁移无需任何迁移代码就能完成这次新增:已有的行取 nil,新的行取初始化方法赋予的值。3
轻量迁移是框架“客气”的那条路径。SwiftData 会检查新的 Schema 与持久化存储,推断出最小的兼容变更并加以应用。整个迁移是自动的,用户毫无察觉,App 照常在既有数据上启动。轻量路径能干净处理的情况有:
- 新增一个可选属性
- 删除一个属性(该列数据被丢弃,后续读取不再看到这一列)
- 重命名框架可以借助提示匹配上的字段(使用
@Attribute(originalName: ...)) - 重命名框架可以匹配上的
@Model类(使用@Model.originalName或提示)
轻量路径会放弃的情况有:
- 向既有 Schema 添加没有默认值的非可选属性(已有的行没有值可以填进去)
- 修改属性的类型(例如
Int→String) - 把一个模型拆成两个,或把两个合并成一个
- 任何需要自定义逻辑才能完成的迁移
当轻量路径放弃时,安全的行为是让迁移失败。不安全的行为则是删库重来;框架足够保守,拒绝悄无声息地那样做。于是用户看到的是 App 启动时因迁移错误而崩溃,开发者看到的是指向 Schema 不匹配的调用栈:没有人丢数据,但所有人都失去了信心。
第一天跳过 VersionedSchema 的代价,会在 v2 → v3 的边界上显现——当您加入第三个功能,而它的 Schema 变更超出了轻量路径的处理能力时。
VersionedSchema 与 MigrationPlan:第一天就要有的纪律
VersionedSchema 声明模型 Schema 的某个具体版本,MigrationPlan 声明如何从一个版本迁移到下一个版本。4 形态如下:
import SwiftData
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV1.self]
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] = [ShoppingItemV2.self]
}
enum AppMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] = [
SchemaV1.self,
SchemaV2.self,
]
static var stages: [MigrationStage] = [
MigrationStage.lightweight(fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
]
}
模型类本身则搬进版本化 Schema 的命名空间:
extension SchemaV1 {
@Model
final class ShoppingItemV1 { /* v1 fields */ }
}
extension SchemaV2 {
@Model
final class ShoppingItemV2 { /* v2 fields, including lastModified */ }
}
构造 ModelContainer 时带上迁移方案:
let container = try ModelContainer(
for: ShoppingItemV2.self,
migrationPlan: AppMigrationPlan.self,
configurations: ModelConfiguration("ShoppingList")
)
迁移方案为框架提供了一张带类型的图,描述 Schema 如何演进。当搭载 v2 的 App 面对 v1 数据库启动时,框架会沿着迁移方案逐级走过声明的阶段,把数据库带到 v2。等到发布 v3,只需在 schemas 里加上 SchemaV3.self,再在 v2 与 v3 之间补一个新的 MigrationStage。完整的迁移模型——哪些变更是自动的、哪些需要显式声明阶段,以及声明了一个本不需要的 V2 会撞上的校验和崩溃——是姊妹篇《SwiftData 迁移:轻量与自定义》的主题。
纪律就是:即便只有一个版本,也要在 v1 里就带上 VersionedSchema。这样做的成本是多一个文件、多一句 enum 声明。不这样做的成本,则是 v2 的第一次不平凡 Schema 变更迫使您回过头把 v1 包进 VersionedSchema——这能做到,但必须精确还原 v1 的形态,框架才能把既有数据认作 SchemaV1。这笔税,要么由做 v2 的未来的您来付,要么由现在的您一次付清、从此不必再想。
用自定义 MigrationStage 应对困难场景
轻量迁移覆盖了大多数增量式变更。类型变更、拆分、合并以及有条件的数据填充,则需要 MigrationStage.custom:
static var stages: [MigrationStage] = [
MigrationStage.custom(
fromVersion: SchemaV1.self,
toVersion: SchemaV2.self,
willMigrate: { context in
// Read v1 rows; stage any derived state to a transient store
// (UserDefaults / temp file) since the v1 and v2 contexts do
// not share state, and didMigrate cannot read v1.
let v1Items = try context.fetch(FetchDescriptor<ShoppingItemV1>())
stageDerivedState(from: v1Items)
},
didMigrate: { context in
// Populate v2-only fields on existing rows
let v2Items = try context.fetch(FetchDescriptor<ShoppingItemV2>())
for item in v2Items where item.lastModified == nil {
item.lastModified = Date()
}
try context.save()
}
)
]
这两个闭包分别在框架执行结构性迁移之前和之后触发。willMigrate 面对的是 v1 Schema,didMigrate 面对的是 v2 Schema。闭包体内就是普通的 SwiftData 代码(fetch descriptor、模型上下文保存,与运行中的 App 里用的是同一套 API),只不过操作的是一个迁移期间的临时上下文。
能在生产环境活下来的模式是:让 willMigrate 保持为空,把所有填充逻辑放进 didMigrate。在 willMigrate 里读取 v1 数据是允许的,但从框架的视角看 v2 Schema 尚不存在,因此任何计算结果都必须暂存到一个 didMigrate 闭包能够读取的临时存储中。更简单的规则是:结构性迁移归框架管;为已有的行填充 v2 才有的字段,归 didMigrate 管。
@Attribute 与 @Relationship 何时名副其实
在 @Model 类里,Schema 的修饰工作大部分由两个宏完成。
@Attribute 为单个属性附加约束或提示:
@Attribute(.unique)强制唯一性,例如ShoppingItem.id@Attribute(.externalStorage)把大块Data二进制内容存放到数据库之外(图片数据、音频缓冲)@Attribute(originalName: "old_field_name")在迁移时把属性对应到被重命名的列@Attribute(.transformable(by: ...))为非 Codable 类型套用ValueTransformer
正确的纪律是:只对真正应当唯一的字段使用 .unique(您生成的 UUID、外部导入的 ID);只要二进制块超过几 KB 就使用 .externalStorage;当 v2 重命名某个属性会导致 v1 数据丢失时,使用 originalName。
@Relationship 修饰指向另一个 @Model 类(或其集合)的属性:
@Model
final class List {
var name: String
@Relationship(deleteRule: .cascade, inverse: \ShoppingItem.list)
var items: [ShoppingItem] = []
}
@Model
final class ShoppingItem {
var name: String
var list: List?
}
deleteRule: .cascade 意味着删除父级 List 时会连带删除它下面所有的 ShoppingItem 行。inverse: 参数告诉框架:子级的哪个属性指回父级——框架据此进行可预测的双向维护。SwiftData 有时能自动推断反向关系,也支持用 inverse: nil 表示明确的单向关系,但安全的默认做法是:只要推断可能产生歧义,就显式声明 inverse:。5
正确的纪律是:声明关系时显式写出 deleteRule(默认值是 .nullify,而它很少是您想要的),并在关系为双向时显式声明 inverse:(而不是依赖框架的推断)。隐式默认值往往是错的;显式写法只多一个参数,却能永久省掉一个 bug。
跨越 actor 边界:传标识符,不要传对象图
@Model 类不是 Sendable,正确的做法是别再试图把它变成 Sendable。实例是指向 ModelContext 所持有的活跃对象图的一个引用;框架无法保证这张图可以安全地从另一个 actor 读取,因此这个类型是被刻意留作非 Sendable 的。强行让它遵循协议并不会让数据竞争消失,只会把它藏起来。7
行之有效的模式是:传递标识与纯值,再到另一侧重新取回。PersistentIdentifier 是 Sendable,能干净地跨过边界。把目标端需要的标量值取出来(一个名称、一个标志位、装在小结构体里的一份差量),连同标识符一起传过去,让接收方 actor 用这个标识符从自己的上下文里重新取回模型:
// On the source actor: extract identity + plain values, never the model.
let id: PersistentIdentifier = item.persistentModelID
let snapshot = ItemSnapshot(name: item.name, isChecked: item.isChecked)
// On the destination actor: re-fetch from this context, then mutate.
let fetched = destinationContext.model(for: id) as? ShoppingItem
要避免的失败模式是把模型对象图本身传过去。当图的一部分跨过边界,接收方拿到的是一个只在对端部分填充的模型:那些在源上下文中从未被触发加载的关系和惰性属性,会去错误的上下文里解析(或者根本解析不出来),随之而来的都是那种不吭声的 bug。安全的契约是“标识符加上抽取出的值”,而不是对象图。ModelActor 正是把这条纪律封装了起来——它自己持有一个上下文,对外交付的是值而不是实例。7
CloudKit 同步与 App Group 授权陷阱
把 SwiftData 存储挪进 App Group 容器、好让小组件或扩展能够读取它,这件事与 CloudKit 同步之间存在一种会在上线之后咬人的相互作用。两个事实可以推出其余的一切。
第一,存储的位置。使用默认的 ModelConfiguration 时,当 App 从“没有 App Group”演进为“使用 App Group”,SwiftData 会替您把既有存储复制进 App Group 容器;Apple 的原话是 SwiftData “copies the existing store to the app group container”(把既有存储复制到 App Group 容器)。8 而使用自定义存储 URL 时,位置归您管:由您把文件复制到新容器,并让配置指向它。默认路径之所以省事,恰恰是因为复制由框架代劳;自定义路径则用这份省事换来了控制权。
第二,授权(entitlement)。凡是要读取 CloudKit 同步存储的 App Group 成员,都必须携带同一份 CloudKit entitlement,因为这些进程每一个都会以自己的名义去同步那个容器。这个要求正是陷阱所在:小组件或扩展既没有足够的运行时预算,也没有前台窗口去驱动一场啰嗦的同步,而把 CloudKit entitlement 交给它,等于逼它去尝试。解法是拆成两个 ModelConfiguration:一个是同步存储(带 CloudKit entitlement,归主 App 所有),另一个是位于 App Group 容器里的本地存储,供小组件与扩展读取、永不参与同步。把同步放在前台 App 能做好的地方,把“共享供读取”的数据挡在同步路径之外。8
如果重来我会怎么做
这一组 App 要么已经采用、要么后悔没有采用的三种模式。
从 v1 就带上 VersionedSchema。 每一个要上线的 @Model 类,第一天就应该住在某个 VersionedSchema 里。成本是每个 Schema 版本多一层包裹用的 enum。收益是 v2 的第一次不平凡变更只需在 MigrationPlan.schemas 里加一行,而不是花两天做回溯式重构。
所有时间戳都设为可选。 lastModified、createdAt、updatedAt 这类为跨设备同步或冲突解决而存在的字段,如果 v1 的产品形态用不到,就应该在 v1 里保持可选。可选性能让(真正需要它们时的)v2 迁移保持廉价。在 didMigrate 里为已有的行填值只是一个循环;而从 v1 起就设为非可选,则是一条可能让用户数据回填失败的约束。
用 UUID 作自然键,而不是 PersistentIdentifier。 SwiftData 的 PersistentIdentifier 只在进程内有效。跨设备同步、MCP 集成(详见两个智能体生态,一份购物清单)以及任何进程外引用,都需要一个稳定的标识符。带 @Attribute(.unique) 的 UUID 才是正确的形态;进程内的 PersistentIdentifier 对任何跨越进程边界的场景都是错的。
@Model 在什么时候是错误答案
以下三种情况里,SwiftData 都不是合适的工具:
单条记录的键值状态。 App 设置、用户选择的语言、上一次同步的时间戳。请使用 UserDefaults 或 NSUbiquitousKeyValueStore(详见五个 Apple 平台,三个共享文件)。为一行数据付出 SwiftData 的开销纯属徒增仪式;键值存储才是合适的底座。
以服务器为准、没有离线写入的数据。 从 REST API 拉取、只读展示的列表。如果真相之源在服务器、本地缓存就只是缓存,SwiftData 就是杀鸡用牛刀。放在 Documents/ 里的一份简单 Codable 快照,加上一个内存缓存数组就够了;既然数据本来就熬不过一次彻底重置,那份 SwiftData 迁移税就不值得交。
多进程协作。 SwiftData 只在单个进程内工作。运行在 iOS App 之外的 MCP 服务器,既读不了也写不了 App 的 SwiftData 容器。跨进程状态需要另一种形态:iCloud Drive 上的 JSON 文件、共享的 App Group 容器,或者一层显式的、为进程之间搭桥的同步层。(Get Bananas 把 SwiftData 与 iCloud Drive JSON 搭配使用,正是出于这个原因。)6
数据是很少变动的大块二进制内容。 一个 10MB 的音频文件、一份 50MB 的图片数据集。如果这些二进制块位于 SwiftData 的行内,就使用 @Attribute(.externalStorage);否则直接使用文件系统,在 SwiftData 里只保存指向文件 URL 的元数据。
Core Data 仍然胜出的场景
SwiftData 是 Core Data 之上的一层,而不是它的全面替代品。三年过去,仍有一批特定任务属于那个更老的框架。为这些场景选择 Core Data 不是历史包袱式的决定,而是当下正确的决定。
数据库侧聚合。 Core Data 有基于 NSExpression 的查询,能把 sum、average、min、max 下推给 SQLite,让数据库在不加载行的前提下算出结果,SwiftData 则没有对应物9。在 SwiftData 里,您只能取回数据再在内存中归约,面对大表时这就失去了意义。文档给出的逃生舱是共存:Apple 描述了运行“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”(两套完全独立的持久化栈,一套 Core Data、一套 SwiftData,连接同一个持久化存储)的做法,让 Core Data 那一侧针对 SwiftData 拥有的那个文件执行下推到 SQL 的聚合9。具体机制,包括对 NSPersistentHistoryTrackingKey 的要求,参见《SwiftData 的性能问题其实是存储问题》。
共享与 CloudKit 公共数据库。 SwiftData 的自动 iCloud 同步在底层依托 NSPersistentCloudKitContainer,而它所配置的容器只会把您的存储镜像到用户的私有 CloudKit 数据库10。通过 CKShare 实现不同 iCloud 用户之间的协作,以及向公共数据库发布内容,都是有文档记载的 Core Data + CloudKit 能力;截至 iOS 27 beta,SwiftData 层面并没有对应的 API10。一款以共享清单或协作文档为核心功能的 App,要么在同步存储上退回 Core Data,要么手工搭建 CloudKit 层。
存储层面的批量更新。 Core Data 的 NSBatchUpdateRequest 无需加载对象,就能直接在存储中改写匹配的行11。SwiftData 的 ModelContext 只有删除那一半(delete(model:where:) 接受一个谓词),没有批量更新的对应物;因此在 SwiftData 里做大规模字段改写,意味着把每一个受影响的模型都实例化出来。
部署下限低于 iOS 17。 SwiftData 要求 iOS 17;Core Data 则能一路向下兼容到任何 App 仍在支持的版本,它的 CloudKit 容器最低可回溯到 iOS 131012。系统尾巴很长的代码库,没有选择的余地。
值得点名的是从这份清单上掉下去的一条:“在视图之外需要 NSFetchedResultsController”曾经是 Core Data 的优势,直到 iOS 27 beta 加入了 ResultsObserver——它通过 Swift Observation 在 App 的任何位置观察一次查询13。这份差距清单正在一个版本一个版本地缩短。押注的方式是:新 App 从 SwiftData 起步,为上述特定任务采用共存方案,只有当共享、公共数据库或部署下限逼迫时,才把完整的 Core Data 栈当作答案。
这套模式对发布在 iOS 26+ 上的 App 意味着什么
三点结论。
-
宏是容易的部分,迁移才是成本。
@Model和@Attribute是两行声明,背后藏着大量 Core Data 管道代码。在 App 的整个生命周期里,您真正付出的是迁移纪律;设计 v1 时就要想着 v2。 -
对要上线的 App,第一天就用
VersionedSchema没有商量余地。 那层包裹用的enum只是多一个文件;事后回补的代价要高得多。 -
可选字段与显式关系是廉价的保险。 同步元数据使用可选时间戳,关系上显式写出
deleteRule与inverse:。两者都只是极小的声明,却能换来 v2 阶段大量的回旋余地。
完整的 Apple 生态系列:面向 Apple Intelligence 的类型化 App Intents;面向跨 LLM 智能体的 MCP 服务器;两者之间的路由取舍;用于端侧 LLM 与 Tool 协议的 Foundation Models;iOS 锁定屏幕状态机背后的实时活动;Apple Watch 上的 watchOS 运行时契约;作为框架底座的 SwiftUI 内部机制;visionOS 场景背后的 RealityKit 空间心智模型;视觉层的 Liquid Glass 模式;实现跨设备触达的多平台发布。系列主页在 Apple 生态系列。关于 iOS 与 AI 智能体的更广背景,请参阅 iOS 智能体开发指南。
常见问题
@Model 与 Core Data 的 NSManagedObject 有什么区别?
@Model 是一个 Swift 宏,会在底层生成 NSManagedObject 的管道代码。SwiftData 以 Core Data 作为后端存储,因此运行时模型是同一套,区别只在表层。@Model 省掉了 .xcdatamodeld 文件、值转换器的繁文缛节,以及 NSManagedObjectContext 的生命周期管理。您得到的是同一个持久化存储,只是换上了一层 Swift 风格的 API。
如果我根本不打算改动 Schema,还需要 VersionedSchema 吗?
如果您的 App 有可能发布 v2,那就需要;如果它只是一次性的演示,那就不需要。从 v1 起使用 VersionedSchema 的成本是多一句 enum 声明;等到 v2 才回补的成本,则是要精确还原 v1 的 Schema 形态,好让框架认出既有数据——这能做到,但很容易出错。大多数正式上线的 App 迟早都会需要一次 Schema 变更;请在 v1 就为它留出预算。
什么时候该使用 @Attribute(.unique)?
当这个字段是该行的自然键时:您生成的 UUID、导入的外部 ID、您指定的 slug。SwiftData 把 .unique 当作 upsert 处理:如果插入的模型带有一个已经存在的 .unique 值,框架会更新已有的那一行,而不是追加一条新记录。正是这个语义让 upsert 式的同步路径(同一个 UUID 来自两台设备)变得安全;同样也正因如此,.unique 不适合用在 title 这类展示名称字段上——两个用户输入相同的标题,会导致他们的记录被悄悄合并,而不是生成两条不同的记录。
向既有 Schema 添加非可选字段时该怎么处理?
使用带 didMigrate 闭包的 MigrationStage.custom,在闭包里为已有的行填充该字段。或者更省事:在新的 Schema 版本里把该字段声明为可选,在访问时惰性填充。可选是更廉价的迁移;新增非可选字段则需要显式的填充逻辑。
PersistentIdentifier 和我自己的 UUID 有什么不同?
PersistentIdentifier 是 SwiftData 的进程内行 ID,由框架自动生成,其有效期与运行中的进程相同。而带 @Attribute(.unique) 的自有 UUID 是稳定的跨进程、跨设备标识符。App 内部的进程内引用使用 PersistentIdentifier;任何跨越进程边界的场景(跨设备同步、外部集成、MCP 工具、网络调用)都使用 UUID。
什么时候仍然应该选择 Core Data 而不是 SwiftData?
截至 iOS 27 beta 有四种情况:数据库侧聚合(SwiftData 缺少的 NSExpression 查询)、通过 CKShare 或 CloudKit 公共数据库在 iCloud 用户之间共享(SwiftData 的同步只覆盖私有数据库)、存储层面的批量更新(NSBatchUpdateRequest),以及部署目标低于 iOS 1791011。聚合这一项并不需要您放弃 SwiftData:让一套共存的 Core Data 栈针对同一个存储文件运行即可。
参考资料
-
作者的 Get Bananas,一款 SwiftUI 购物清单 App,把 SwiftData 与 iCloud Drive JSON 同步以及一个 MCP 服务器搭配使用。
ShoppingItem模型在早期开发周期中不断演进;lastModified: Date?字段是在最初的 Schema 之后才加入的(2025年12月1日的提交268a00d,“Make lastModified optional to fix migration crash”),因为把它设为非可选,会在已有的行没有值可填时破坏迁移。 ↩ -
Apple Developer,“SwiftData” 与 “Adding and editing persistent data in your app”。
@Model宏、@Attribute的约束接口,以及它与 Core DataNSManagedObjectModel的关系。 ↩↩ -
Apple Developer,“Preserving your app’s model data across launches” 与 “Adopting SwiftData for a Core Data app”。轻量迁移的语义,以及哪些情况会让框架放弃迁移。 ↩
-
Apple Developer,“VersionedSchema” 与 “SchemaMigrationPlan”。版本化 Schema 的声明、迁移阶段的定义,以及接受迁移方案的
ModelContainer构造器。 ↩ -
Apple Developer,“Defining data relationships with enumerations and model classes” 与 “Schema.Relationship”。
@Relationship宏、deleteRule的可选值(.cascade、.nullify、.deny、.noAction),以及inverse:参数在双向关系维护中的作用。 ↩ -
作者在两个智能体生态,一份购物清单(2026年4月29日)与五个 Apple 平台,三个共享文件中的分析。Get Bananas 与 Return 所采用的跨进程、跨设备同步模式,在多进程工作流中补足(有时甚至替代)SwiftData。 ↩
-
Apple Developer,“PersistentIdentifier”(遵循
Sendable)与 “ModelActor”。SwiftData 团队在 WWDC 2026 SwiftData Group Lab 上确认:@Model对象不是Sendable,也不应被强行让其遵循该协议,因为它们是活在某个上下文内部的引用图;推荐的边界契约是传递作为Sendable的PersistentIdentifier加上抽取出的纯值,并在目标上下文中重新取回,而传递模型对象图会让接收方拿到一个只被部分填充的对象。以上内容转述自本地转录的 WWDC 2026 SwiftData Group Lab 录音;Apple 并未为这些 Lab 发布官方字幕。 ↩↩ -
Apple Developer,“Adopting SwiftData for a Core Data app”,其中写道:在默认配置下“SwiftData copies the existing store to the app group container”(SwiftData 会把既有存储复制到 App Group 容器),而使用自定义存储 URL 时,位置则交由您自行管理。App Group 成员必须携带 CloudKit entitlement 的要求,以及用两个
ModelConfiguration拆分(一个同步、一个本地)来让小组件与扩展远离同步路径的做法,均在 WWDC 2026 SwiftData Group Lab 上有过说明。以上内容转述自本地转录的 WWDC 2026 SwiftData Group Lab 录音;Apple 并未为这些 Lab 发布官方字幕。 ↩↩ -
Apple,WWDC 2023 session 10189,“Migrate to SwiftData”,共存说法的出处(“two completely separate persistent stacks, one Core Data stack and one SwiftData stack, talking to the same persistent store”);以及 Apple Developer,“NSExpression”,即 Core Data 把聚合下推到 SQL 的机制,而 SwiftData 没有提供对应物。这一缺口经 WWDC 2026 SwiftData Group Lab 上的 SwiftData 工程团队小组确认(转述自本地转录的录音)。 ↩↩↩
-
Apple Developer,“Syncing model data across a person’s devices”,其中写道“SwiftData uses the
NSPersistentCloudKitContainerclass from Core Data to handle CloudKit synchronization”(SwiftData 使用 Core Data 的NSPersistentCloudKitContainer类来处理 CloudKit 同步);“NSPersistentCloudKitContainer”(iOS 13.0+),其摘要描述了把“select persistent stores to a CloudKit private database”(选定的持久化存储镜像到 CloudKit 私有数据库);以及“Sharing Core Data objects between iCloud users”,即基于CKShare的协作在 Core Data 中有文档记载的实现路径。截至 iOS 27 beta,SwiftData 的文档没有暴露任何共享或公共数据库 API。 ↩↩↩↩ -
Apple Developer,“NSBatchUpdateRequest”,以及 “ModelContext.delete(model:where:includeSubclasses:)”,即 SwiftData 基于谓词的批量删除。SwiftData 的
ModelContext文档中并未列出批量更新的对应物。 ↩↩ -
平台可用性依据 Apple Developer 文档:SwiftData(iOS 17.0+)与 Core Data(iOS 3.0+)。 ↩
-
Apple Developer,“ResultsObserver”(iOS 27.0 beta),它“observes and tracks changes to a collection of persistent models in a model context”(观察并追踪模型上下文中一组持久化模型的变化)且遵循
Observable,承担了此前需要 Core DataNSFetchedResultsController才能完成的、在视图之外进行观察的角色。 ↩