Highlight
SwiftData 通过
@Model、@Attribute、@Relationship等宏让数据建模完全代码化。开发者可以用 Swift 代码直接定义数据库 Schema、配置持久化行为、管理版本迁移,不需要任何外部文件格式。
核心内容
从 Core Data 到 SwiftData:建模方式的转变
以前用 Core Data 建模,需要打开 .xcdatamodeld 文件,在图形界面里添加 Entity、配置 Attribute、设置 Relationship。模型和代码分离,改一处要同步另一处,容易出错。
SwiftData 把建模完全搬进代码。给一个 Swift 类加上 @Model 宏,它就自动获得持久化能力。
(00:56)@Model 宏让 Trip 类自动遵循 PersistentModel 协议,框架根据属性定义生成完整的 Schema。代码即真理,模型定义就是 Schema 的单一来源。
@Attribute:精细控制属性行为
(01:50)发布应用后发现同名 Trip 造成冲突。@Attribute(.unique) 可以确保属性值唯一,保存时执行 upsert 语义。想把 start_date 改成 startDate,但直接改名会被视为新属性导致数据丢失,@Attribute 的 originalName 参数可以映射新旧属性名,避免数据丢失并实现平滑迁移。
详细内容
@Attribute:唯一约束与 upsert
(01:50)发布应用后发现同名 Trip 造成冲突。用 @Attribute(.unique) 可以确保属性值唯一:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
关键点:
@Attribute(.unique)确保name字段值唯一- 保存时如果存在同名 Trip,执行 upsert(先尝试插入,冲突则更新)
- 唯一约束适用于基础类型(数字、字符串、UUID)和一对一关系
@Attribute:属性重命名映射
(02:48)想把 start_date 改成 startDate,但直接改名会被视为新属性,导致数据丢失。用 originalName 参数映射:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
关键点:
originalName把新属性名映射到数据库中的旧列名- 避免数据丢失,实现平滑迁移
@Attribute还支持.externalStorage(大数据存外部文件)和 transformable 类型
@Relationship:管理对象关系
级联删除
(04:00)默认删除规则是 nullify——删除 Trip 时,关联的 bucketList 和 livingAccommodation 的外键被设为 nil,变成孤儿数据。想要删除 Trip 时一并删除关联数据:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
@Relationship(.cascade)
var bucketList: [BucketListItem]? = []
@Relationship(.cascade)
var livingAccommodation: LivingAccommodation?
}
关键点:
@Relationship(.cascade)删除父对象时自动删除所有子对象- 隐式反向关系自动发现,不需要额外注解
@Relationship还支持originalName和设置 to-many 关系的最小/最大数量
@Transient:排除不需要持久化的属性
(04:54)想记录 Trip 被查看的次数,但不需要持久化:
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
@Relationship(.cascade)
var bucketList: [BucketListItem]? = []
@Relationship(.cascade)
var livingAccommodation: LivingAccommodation?
@Transient
var tripViews: Int = 0
}
关键点:
@Transient标记的属性不会存入数据库- 必须提供默认值,确保从 SwiftData 获取对象时有合理的初始值
- 适合缓存、临时计算结果等不需要持久化的数据
Schema 迁移:版本演进的正确方式
(07:12)应用发布多个版本后,Schema 需要演进。SwiftData 用 VersionedSchema 定义不同版本的 Schema:
enum SampleTripsSchemaV1: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
}
enum SampleTripsSchemaV2: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
var start_date: Date
var end_date: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
}
enum SampleTripsSchemaV3: VersionedSchema {
static var models: [any PersistentModel.Type] {
[Trip.self, BucketListItem.self, LivingAccommodation.self]
}
@Model
final class Trip {
@Attribute(.unique) var name: String
var destination: String
@Attribute(originalName: "start_date") var startDate: Date
@Attribute(originalName: "end_date") var endDate: Date
var bucketList: [BucketListItem]? = []
var livingAccommodation: LivingAccommodation?
}
}
关键点:
- 每个版本是一个独立的
VersionedSchema枚举 models属性列出该版本包含的所有模型类型- 版本类内部定义该版本的
@Model类
(07:49)定义迁移计划:
enum SampleTripsMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[SampleTripsSchemaV1.self, SampleTripsSchemaV2.self, SampleTripsSchemaV3.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2, migrateV2toV3]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: SampleTripsSchemaV1.self,
toVersion: SampleTripsSchemaV2.self,
willMigrate: { context in
let trips = try? context.fetch(FetchDescriptor<SampleTripsSchemaV1.Trip>())
// De-duplicate Trip instances here...
try? context.save()
}, didMigrate: nil
)
static let migrateV2toV3 = MigrationStage.lightweight(
fromVersion: SampleTripsSchemaV2.self,
toVersion: SampleTripsSchemaV3.self
)
}
关键点:
schemas列出所有版本,按顺序排列stages定义每两个版本之间的迁移步骤.custom迁移需要手动编写willMigrate闭包处理数据转换.lightweight迁移由框架自动处理(如属性重命名)willMigrate在 Schema 变更前执行,可以查询旧版本数据做预处理didMigrate在 Schema 变更后执行,可以验证迁移结果
(08:40)在应用启动时配置迁移计划:
struct TripsApp: App {
let container = ModelContainer(
for: Trip.self,
migrationPlan: SampleTripsMigrationPlan.self
)
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
关键点:
ModelContainer初始化时传入migrationPlan- 框架自动检测当前数据库版本,按顺序执行需要的迁移
- 迁移在后台线程执行,完成后应用正常启动
核心启发
用唯一约束简化数据同步
- 做什么:为具有业务唯一性的字段(如用户名、订单号)添加
@Attribute(.unique) - 为什么值得做:upsert 语义让同步逻辑更简单,不需要先查询再决定插入还是更新
- 怎么开始:在
@Model类中为关键字段添加@Attribute(.unique)
为大数据属性启用外部存储
- 做什么:图片、音频等二进制数据使用
@Attribute(.externalStorage) - 为什么值得做:减少数据库文件体积,提升查询性能,大数据存取更高效
- 怎么开始:将图片等属性标记为
@Attribute(.externalStorage),SwiftData 自动管理文件存储
设计合理的删除级联策略
- 做什么:明确每个父子关系的删除行为,用
@Relationship(.cascade)或.nullify - 为什么值得做:避免孤儿数据,保持数据完整性
- 怎么开始:分析数据模型中的关系,为需要级联删除的关系添加
@Relationship(.cascade)
尽早建立版本化 Schema 和迁移计划
- 做什么:从第一个版本开始就使用
VersionedSchema,即使当前不需要迁移 - 为什么值得做:后期补迁移计划比一开始就规划要困难得多
- 怎么开始:定义
SchemaV1,即使内容很简单。后续版本递增编号,每次变更都更新迁移计划
关联 Session
- Meet SwiftData — SwiftData 框架入门介绍
- Build an app with SwiftData — 用 SwiftData 构建完整应用的实战演示
- Dive deeper into SwiftData — SwiftData 高级查询和性能优化
评论
GitHub Issues · utterances