关于 swiftdata
面向 iOS 开发者的 SwiftData 持久化技能,讲解模型、查询、迁移、并发及 SwiftUI 集成,也覆盖 CloudKit 同步和 Core Data 共存边界。
主要功能
- 01定义模型与配置存储容器
- 02执行 CRUD 和谓词查询
- 03管理模式版本与迁移
- 04使用 @ModelActor 处理并发
- 05集成 SwiftUI 与 @Query
- 06检查 CloudKit 同步配置
适用场景
- 01为 iOS 应用设计持久化模型
- 02从 Core Data 分阶段迁移
- 03排查 CloudKit 模式兼容问题
- 04在后台安全执行数据操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill swiftdata安装后在 Claude Code 中输入/swiftdata确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/core-data-coexistence.mdindexing.mdpredicate-pitfalls.mdswiftdata-advanced.mdswiftdata-queries.md详细介绍
SwiftData
使用 Swift 6.3,在 iOS 26+ 应用中持久化、查询和管理结构化数据。
目录
- 模型定义
- ModelContainer 设置
- CloudKit 同步
- CRUD 操作
@Query in SwiftUI- #Predicate
- FetchDescriptor
- 架构版本控制与迁移
- Core Data 共存边界
- 并发(
@ModelActor) - SwiftUI 集成
- 常见错误
- 审查清单
- 参考资料
模型定义
将 @Model 应用于类(而非 struct)。它会合成 PersistentModel
一致性。模型实例仍绑定到 context/actor;跨 actor 传递时应传递其
PersistentIdentifier,而非实例本身。
@Model
class Trip {
var name: String
var destination: String
var startDate: Date
var endDate: Date
var isFavorite: Bool = false
@Attribute(.externalStorage) var imageData: Data?
@Relationship(deleteRule: .cascade, inverse: \LivingAccommodation.trip)
var accommodation: LivingAccommodation?
@Transient var isSelected: Bool = false // Always provide default
init(name: String, destination: String, startDate: Date, endDate: Date) {
self.name = name; self.destination = destination
self.startDate = startDate; self.endDate = endDate
}
}
@Attribute 选项:.externalStorage、.unique、.spotlight、.allowsCloudEncryption、.preserveValueOnDeletion、.ephemeral、.transformable(by:)。重命名:@Attribute(originalName: "old_name")。
@Relationship:deleteRule: 可设为 .cascade/.nullify(默认)/.deny/.noAction。指定 inverse: 以确保行为可靠。单向关系(iOS 18+):inverse: nil。
#Unique(iOS 18+):#Unique<Person>([\.firstName, \.lastName]) -- 复合唯一性约束。
继承(iOS 26+):@Model class BusinessTrip: Trip { var company: String }。
支持的类型:Bool、Int/UInt 变体、Float、Double、String、Date、Data、URL、UUID、Decimal、Array、Dictionary、Set、Codable 枚举、Codable 结构体和其他兼容的 Codable 值类型,以及与 @Model 类的关系。
ModelContainer 设置
// Basic
let container = try ModelContainer(for: Trip.self, LivingAccommodation.self)
// Configured
let config = ModelConfiguration("Store", isStoredInMemoryOnly: false,
groupContainer: .identifier("group.com.example.app"),
cloudKitDatabase: .private("iCloud.com.example.app"))
let container = try ModelContainer(for: Trip.self, configurations: config)
// With migration plan
let container = try ModelContainer(for: SchemaV2.Trip.self,
migrationPlan: TripMigrationPlan.self)
// In-memory (previews/tests)
let container = try ModelContainer(for: Trip.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true))
CloudKit 同步
ModelConfiguration(..., cloudKitDatabase:) 可为 SwiftData 存储启用自动 CloudKit 同步,但同步仍受应用权限配置限制。
对于任何 SwiftData CloudKit 配置或架构审查任务,请在架构审查结果前单独给出 Capabilities 判定:
- Capabilities:Xcode target 已启用 iCloud capability 和 CloudKit,并选择了预期容器,同时已启用 Background Modes > Remote notifications。缺少这些权限时,即使设置了
cloudKitDatabase,自动同步也未完全配置。 - 架构兼容性:不得使用
@Attribute(.unique)或#Unique;关系应为可选,在需要时显式设置反向关系,并避免使用.deny;大型Data使用@Attribute(.externalStorage)。 - 标量属性:不要仅为适配 CloudKit 就将所有标量设为可选。如果初始化器、默认值或迁移能提供有效值,则应将必需标量保留为非可选。
- 架构发布:仅在非生产版本中初始化开发架构,在 CloudKit Dashboard 中验证,并在发布前提升;生产环境的变更应仅采用增量方式。
CRUD 操作
对于破坏性批处理和迁移,先在一次性副本上运行确切的谓词或版本迁移步骤,并记录受影响的标识符/数量。使用明确的事务/保存语义执行,然后重新获取数据,并验证值、关系、数量和不变量。失败时,修正谓词/架构,并在重试前还原干净的测试数据;切勿盲目重放破坏性操作。
// CREATE
let trip = Trip(name: "Summer", destination: "Paris", startDate: .now, endDate: .now + 86400*7)
modelContext.insert(trip)
try modelContext.save() // or rely on autosave
// READ
let trips = try modelContext.fetch(FetchDescriptor<Trip>(
predicate: #Predicate { $0.destination == "Paris" },
sortBy: [SortDescriptor(\.startDate)]))
// UPDATE -- modify properties directly; autosave handles persistence
trip.destination = "Rome"
// DELETE
modelContext.delete(trip)
try modelContext.delete(model: Trip.self, where: #Predicate { $0.isFavorite == false })
// TRANSACTION (atomic)
try modelContext.transaction {
modelContext.insert(trip); trip.isFavorite = true
}
@Query in SwiftUI
struct TripListView: View {
@Query(filter: #Predicate<Trip> { $0.isFavorite == true },
sort: \.startDate, order: .reverse)
private var favorites: [Trip]
var body: some View { List(favorites) { trip in Text(trip.name) } }
}
// Dynamic query via init
struct SearchView: View {
@Query private var trips: [Trip]
init(search: String) {
_trips = Query(filter: #Predicate<Trip> { trip in
search.isEmpty || trip.name.localizedStandardContains(search)
}, sort: [SortDescriptor(\.name)])
}
var body: some View { List(trips) { trip in Text(trip.name) } }
}
// FetchDescriptor query
struct RecentView: View {
static var desc: FetchDescriptor<Trip> {
var d = FetchDescriptor<Trip>(sortBy: [SortDescriptor(\.startDate)])
d.fetchLimit = 5; return d
}
@Query(RecentView.desc) private var recent: [Trip]
var body: some View { List(recent) { trip in Text(trip.name) } }
}
#Predicate
#Predicate<Trip> { $0.destination.localizedStandardContains("paris") } // String
let now = Date()
#Predicate<Trip> { $0.startDate > now } // Date
#Predicate<Trip> { $0.isFavorite && $0.destination != "Unknown" } // Compound
#Predicate<Trip> { $0.accommodation?.name != nil } // Optional
#Predicate<Trip> { $0.tags.contains { $0.name == "adventure" } } // Collection
支持:==、!=、<、<=、>、>=、&&、||、!、contains()、allSatisfy()、filter()、starts(with:)、localizedStandardContains()、caseInsensitiveCompare()、算术运算、条件表达式、可选链和绑定、nil 合并、类型转换。避免:循环、嵌套声明、变更操作,以及任意不受支持的方法调用。
FetchDescriptor
var d = FetchDescriptor<Trip>(predicate: ..., sortBy: [...])
d.fetchLimit = 20; d.fetchOffset = 0
d.includePendingChanges = true
d.propertiesToFetch = [\.name, \.startDate]
d.relationshipKeyPathsForPrefetching = [\.accommodation]
let trips = try modelContext.fetch(d)
let count = try modelContext.fetchCount(d)
let ids = try modelContext.fetchIdentifiers(d)
try modelContext.enumerate(d, batchSize: 1000) { trip in trip.isProcessed = true }
架构版本控制与迁移
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Trip.self] }
@Model class Trip { var name: String; init(name: String) { self.name = name } }
}
enum SchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] { [Trip.self] }
@Model class Trip {
var name: String; var startDate: Date? // New property
init(name: String) { self.name = name }
}
}
enum TripMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] }
static var stages: [MigrationStage] { [migrateV1toV2] }
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: SchemaV1.self, toVersion: SchemaV2.self)
}
// Custom migration for data transformation
static let migrateV2toV3 = MigrationStage.custom(
fromVersion: SchemaV2.self, toVersion: SchemaV3.self,
willMigrate: nil,
didMigrate: { context in
let trips = try context.fetch(FetchDescriptor<SchemaV3.Trip>())
for trip in trips { trip.displayName = trip.name.capitalized }
try context.save()
})
轻量级处理:添加可选或带默认值的属性、重命名(originalName)、移除属性、添加模型类型。
确认阶段列表涵盖所有受支持的版本跳转,然后迁移每个旧存储的新副本,并在发布前断言迁移后的数据。
Core Data 共存边界
当任务是在现有 Core
Data 存储旁运行 SwiftData,或随时间推移将屏幕从 Core Data 迁移到 SwiftData 时,请使用此技能。纯粹的 Core
Data 堆栈设置、NSManagedObjectContext、NSFetchRequest 和 Core Data 批量操作,请使用同级的
core-data 技能。
共存时,请先提供边界指导,再给出详细的迁移建议:
- 将 SwiftData 和 Core Data 指向同一个 SQLite 存储 URL。
- 在 SwiftData 的
@Model定义中,使 Core Data 实体名称、属性名称、类型和关系结构保持一致。 - 对于持久化的 Core
Data 名称与 Swift 名称不同的 SwiftData 属性,使用
@Attribute(originalName:)。 - 迁移期间,不要让两个堆栈同时写入同一个实体;为每个实体指定一个堆栈作为写入方。
并发(@ModelActor)
@ModelActor
actor DataHandler {
func importTrips(_ records: [TripRecord]) throws {
for r in records {
modelContext.insert(Trip(name: r.name, destination: r.dest,
startDate: r.start, endDate: r.end))
}
try modelContext.save() // Always save explicitly in @ModelActor
}
func process(tripID: PersistentIdentifier) throws {
guard let trip = self[tripID, as: Trip.self] else { return }
trip.isProcessed = true; try modelContext.save()
}
}
let handler = DataHandler(modelContainer: container)
try await handler.importTrips(records)
规则:ModelContainer 是 Sendable。ModelContext 不是——应在创建它的 Actor 上使用。跨边界传递 PersistentIdentifier(Sendable)。绝不要跨 Actor 传递 @Model 对象。
SwiftUI 集成
@main
struct MyApp: App {
var body: some Scene {
WindowGroup { ContentView() }
.modelContainer(for: [Trip.self, LivingAccommodation.self])
}
}
struct DetailView: View {
@Environment(\.modelContext) private var modelContext
let trip: Trip
var body: some View {
Text(trip.name)
Button("Delete") { modelContext.delete(trip) }
}
}
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Trip.self, configurations: config)
container.mainContext.insert(Trip(name: "Preview", destination: "London",
startDate: .now, endDate: .now + 86400))
return TripListView().modelContainer(container)
}
常见错误
1. 在 struct 上使用 @Model——应使用 class。@Model 要求采用引用语义。
2. 使用 @Transient 却不提供默认值——务必提供默认值:@Transient var x: Bool = false。
3. 缺少 .modelContainer——视图层级中没有容器时,@Query 会返回空结果。
4. 跨 Actor 传递模型对象:
// WRONG: await handler.process(trip: trip)
// CORRECT: await handler.process(tripID: trip.persistentModelID)
5. 在错误的 Actor 上使用 ModelContext:
// WRONG: Task.detached { context.fetch(...) }
// CORRECT: Use @ModelActor for background work
6. 使用不受支持的 #Predicate 表达式:
// WRONG: #Predicate<Trip> { $0.name.uppercased() == "PARIS" }
// CORRECT: #Predicate<Trip> { $0.name.localizedStandardContains("paris") }
7. 在 #Predicate 中使用流程控制:
// WRONG: #Predicate<Trip> { for tag in $0.tags { ... } }
// CORRECT: #Predicate<Trip> { $0.tags.contains { $0.name == "x" } }
8. 在 @ModelActor 中不保存——务必显式调用 try modelContext.save()。
9. 将 ObservableObject 与 @Model 搭配使用——绝不要使用 ObservableObject/@Published。@Model 会生成 Observable。在视图中使用 @Query。
10. 非可选关系没有默认值:
// WRONG: var accommodation: LivingAccommodation // crashes on reconstitution
// CORRECT: var accommodation: LivingAccommodation?
11. 级联删除未指定反向关系——指定 inverse:,以确保级联删除行为可靠。
12. 使用 DispatchQueue 处理后台数据:
// WRONG: DispatchQueue.global().async { ModelContext(container).fetch(...) }
// CORRECT: @ModelActor actor Handler { func fetch() throws { ... } }
审核清单
- 每个
@Model都是带有指定初始化器的 class - 所有
@Transient属性都有默认值 - 关系指定了
deleteRule和inverse - 在场景或根视图层级附加
.modelContainer - 在 SwiftUI 中使用
@Query响应式显示数据 -
#Predicate仅使用受支持的运算符 - 后台任务使用
@ModelActor - 跨 Actor 边界使用
PersistentIdentifier - 架构变更包含
VersionedSchema+SchemaMigrationPlan - 大型数据使用
@Attribute(.externalStorage) - CloudKit 模型避免唯一性约束,使用可选关系,避免
.deny,且不要将所有标量属性都设为可选 - 检查 CloudKit 同步是否配置了 iCloud + CloudKit、Remote notifications,并完成生产架构发布
- 在
@ModelActor方法中显式调用save() - 预览使用
ModelConfiguration(isStoredInMemoryOnly: true) - SwiftUI 视图访问的
@Model类通过@ModelActor或 MainActor 隔离置于@MainActor上
参考资料
- references/swiftdata-advanced.md — 自定义数据存储、历史记录跟踪、CloudKit、复合属性、模型继承、撤销/重做、性能
- references/swiftdata-queries.md —
@Query变体、FetchDescriptor 深入解析、分节查询、动态查询、后台提取 - references/core-data-coexistence.md — Core Data 与 SwiftData 的共存及迁移边界
- references/predicate-pitfalls.md — #Predicate 运行时崩溃、不支持的表达式、安全模式
- references/indexing.md — #Index 宏、复合索引、何时建立索引、迁移