swiftdata by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftdata使用 Swift 6.2 的 SwiftData,在 iOS 26+ 应用中持久化、查询和管理结构化数据。
将 @Model 应用于类(而非结构体)。生成 PersistentModel、Observable、Sendable。
@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 // 始终提供默认值
init(name: String, destination: String, startDate: Date, endDate: Date) {
self.name = name; self.destination = destination
self.startDate = startDate; self.endDate = endDate
}
}
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
@Attribute 选项:.externalStorage、.unique、.spotlight、.allowsCloudEncryption、.preserveValueOnDeletion (iOS 18+)、.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 结构体(复合类型,iOS 18+)、与 @Model 类的关系。
// 基础用法
let container = try ModelContainer(for: Trip.self, LivingAccommodation.self)
// 配置
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)
// 带迁移计划
let container = try ModelContainer(for: SchemaV2.Trip.self,
migrationPlan: TripMigrationPlan.self)
// 内存模式(预览/测试)
let container = try ModelContainer(for: Trip.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true))
// 创建
let trip = Trip(name: "Summer", destination: "Paris", startDate: .now, endDate: .now + 86400*7)
modelContext.insert(trip)
try modelContext.save() // 或依赖自动保存
// 读取
let trips = try modelContext.fetch(FetchDescriptor<Trip>(
predicate: #Predicate { $0.destination == "Paris" },
sortBy: [SortDescriptor(\.startDate)]))
// 更新 —— 直接修改属性;自动保存处理持久化
trip.destination = "Rome"
// 删除
modelContext.delete(trip)
try modelContext.delete(model: Trip.self, where: #Predicate { $0.isFavorite == false })
// 事务(原子性)
try modelContext.transaction {
modelContext.insert(trip); trip.isFavorite = true
}
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) } }
}
// 通过初始化器实现动态查询
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 查询
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<Trip> { $0.destination.localizedStandardContains("paris") } // 字符串
#Predicate<Trip> { $0.startDate > Date.now } // 日期
#Predicate<Trip> { $0.isFavorite && $0.destination != "Unknown" } // 复合条件
#Predicate<Trip> { $0.accommodation?.name != nil } // 可选值
#Predicate<Trip> { $0.tags.contains { $0.name == "adventure" } } // 集合
支持:==、!=、<、<=、>、>=、&&、||、!、contains()、allSatisfy()、filter()、starts(with:)、localizedStandardContains()、caseInsensitiveCompare()、算术运算、三元运算符、可选链、空值合并、类型转换。不支持:流程控制、嵌套声明、任意方法调用。
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? // 新属性
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)
}
// 用于数据转换的自定义迁移
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)、移除属性、添加模型类型。
@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() // 在 @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 对象。
@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. 在结构体上使用 @Model —— 请使用类。@Model 需要引用语义。
2. @Transient 属性没有默认值 —— 始终提供默认值:@Transient var x: Bool = false。
3. 缺少 .modelContainer —— 如果视图层次结构上没有容器,@Query 将返回空结果。
4. 跨 actor 传递模型对象:
// 错误:await handler.process(trip: trip)
// 正确:await handler.process(tripID: trip.persistentModelID)
5. 在错误的 actor 上使用 ModelContext:
// 错误:Task.detached { context.fetch(...) }
// 正确:使用 @ModelActor 处理后台工作
6. 不支持的 #Predicate 表达式:
// 错误:#Predicate<Trip> { $0.name.uppercased() == "PARIS" }
// 正确:#Predicate<Trip> { $0.name.localizedStandardContains("paris") }
7. 在 #Predicate 中使用流程控制:
// 错误:#Predicate<Trip> { for tag in $0.tags { ... } }
// 正确:#Predicate<Trip> { $0.tags.contains { $0.name == "x" } }
8. 在 @ModelActor 中没有保存 —— 始终显式调用 try modelContext.save()。
9. 将 ObservableObject 与 @Model 混用 —— 切勿使用 ObservableObject/@Published。@Model 会生成 Observable。在视图中使用 @Query。
10. 非可选关系没有默认值:
// 错误:var accommodation: LivingAccommodation // 重建时崩溃
// 正确:var accommodation: LivingAccommodation?
11. 级联删除没有指定 inverse —— 指定 inverse: 以获得可靠的级联删除行为。
12. 使用 DispatchQueue 处理后台数据工作:
// 错误:DispatchQueue.global().async { ModelContext(container).fetch(...) }
// 正确:@ModelActor actor Handler { func fetch() throws { ... } }
@Model 都是一个具有指定初始化器的类@Transient 属性都有默认值deleteRule 和 inverse.modelContainer 附加在场景/根视图层级@Query 进行响应式数据显示#Predicate 仅使用支持的运算符@ModelActorPersistentIdentifierVersionedSchema + SchemaMigrationPlan@Attribute(.externalStorage)@ModelActor 方法中显式调用 save()ModelConfiguration(isStoredInMemoryOnly: true)@Model 类通过 @ModelActor 或 MainActor 隔离位于 @MainActor 上references/swiftdata-advanced.md 了解自定义数据存储、历史跟踪、CloudKit、Core Data 共存、复合属性、模型继承、撤销/重做和性能模式。references/swiftdata-queries.md 了解 @Query 变体、FetchDescriptor 深入探讨、分段查询、动态查询和后台获取模式。references/core-data-coexistence.md 了解独立的 Core Data 模式以及从 Core Data 迁移到 SwiftData 的策略。每周安装数
406
仓库
GitHub 星标数
269
首次出现
2026年3月3日
安全审计
安装于
codex403
kimi-cli400
github-copilot400
gemini-cli400
cursor400
opencode400
Persist, query, and manage structured data in iOS 26+ apps using SwiftData with Swift 6.2.
Apply @Model to a class (not struct). Generates PersistentModel, Observable, Sendable.
@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 options : .externalStorage, .unique, .spotlight, .allowsCloudEncryption, .preserveValueOnDeletion (iOS 18+), .ephemeral, .transformable(by:). Rename: @Attribute(originalName: "old_name").
@Relationship : deleteRule: .cascade/.nullify(default)/.deny/.noAction. Specify inverse: for reliable behavior. Unidirectional (iOS 18+): inverse: nil.
#Unique (iOS 18+) : #Unique<Person>([\.firstName, \.lastName]) -- compound uniqueness.
Inheritance (iOS 26+) : @Model class BusinessTrip: Trip { var company: String }.
Supported types: Bool, Int/UInt variants, Float, Double, String, Date, Data, URL, UUID, Decimal, Array, , , enums, structs (composite, iOS 18+), relationships to classes.
// 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))
// 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
}
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<Trip> { $0.destination.localizedStandardContains("paris") } // String
#Predicate<Trip> { $0.startDate > Date.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
Supported: ==, !=, <, <=, >, >=, &&, ||, !, contains(), allSatisfy(), filter(), , , , arithmetic, ternary, optional chaining, nil coalescing, type casting. : flow control, nested declarations, arbitrary method calls.
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()
})
Lightweight handles: adding optional/defaulted properties, renaming (originalName), removing properties, adding model types.
@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)
Rules : ModelContainer is Sendable. ModelContext is NOT -- use on its creating actor. Pass PersistentIdentifier (Sendable) across boundaries. Never pass @Model objects across actors.
@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. @Model on struct -- Use class. @Model requires reference semantics.
2. @Transient without default -- Always provide default: @Transient var x: Bool = false.
3. Missing .modelContainer -- @Query returns empty without a container on the view hierarchy.
4. Passing model objects across actors:
// WRONG: await handler.process(trip: trip)
// CORRECT: await handler.process(tripID: trip.persistentModelID)
5. ModelContext on wrong actor:
// WRONG: Task.detached { context.fetch(...) }
// CORRECT: Use @ModelActor for background work
6. Unsupported #Predicate expressions:
// WRONG: #Predicate<Trip> { $0.name.uppercased() == "PARIS" }
// CORRECT: #Predicate<Trip> { $0.name.localizedStandardContains("paris") }
7. Flow control in #Predicate:
// WRONG: #Predicate<Trip> { for tag in $0.tags { ... } }
// CORRECT: #Predicate<Trip> { $0.tags.contains { $0.name == "x" } }
8. No save in @ModelActor -- Always call try modelContext.save() explicitly.
9. ObservableObject with @Model -- Never use ObservableObject/@Published. @Model generates Observable. Use @Query in views.
10. Non-optional relationship without default:
// WRONG: var accommodation: LivingAccommodation // crashes on reconstitution
// CORRECT: var accommodation: LivingAccommodation?
11. Cascade without inverse -- Specify inverse: for reliable cascade delete behavior.
12. DispatchQueue for background data work:
// WRONG: DispatchQueue.global().async { ModelContext(container).fetch(...) }
// CORRECT: @ModelActor actor Handler { func fetch() throws { ... } }
@Model is a class with a designated initializer@Transient properties have default valuesdeleteRule and inverse.modelContainer attached at scene/root view level@Query used for reactive data display in SwiftUI#Predicate uses only supported operators@ModelActorPersistentIdentifier used across actor boundariesVersionedSchema + references/swiftdata-advanced.md for custom data stores, history tracking, CloudKit, Core Data coexistence, composite attributes, model inheritance, undo/redo, and performance patterns.references/swiftdata-queries.md for @Query variants, FetchDescriptor deep dive, sectioned queries, dynamic queries, and background fetch patterns.references/core-data-coexistence.md for standalone Core Data patterns and Core Data to SwiftData migration strategies.Weekly Installs
406
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex403
kimi-cli400
github-copilot400
gemini-cli400
cursor400
opencode400
Railway 模板部署指南:快速添加 Postgres、Redis、CMS 等服务
716 周安装
DictionarySetCodableCodable@Modelstarts(with:)localizedStandardContains()caseInsensitiveCompare()SchemaMigrationPlan@Attribute(.externalStorage)save() in @ModelActor methodsModelConfiguration(isStoredInMemoryOnly: true)@Model classes accessed from SwiftUI views are on @MainActor via @ModelActor or MainActor isolation