Swiftdata

by dpearson26998d90fd121a26No license1.1K starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 months ago

Implement, review, or improve data persistence using SwiftData. Use when defining @Model classes with @Attribute, @Relationship, @Transient, #Unique, or #Index; when querying with @Query, #Predicate, FetchDescriptor, or SortDescriptor; when configuring ModelContainer and ModelContext for SwiftUI or background work with @ModelActor; when planning schema migrations with VersionedSchema and SchemaMigrationPlan; when setting up CloudKit sync with ModelConfiguration; or when coexisting with or migrating from Core Data.

Instructions onlySoftware Development
AI-generated overview

Guides implementing, reviewing, and migrating SwiftData persistence in iOS apps, covering models, queries, containers, and CloudKit.

What it does
Provides reference guidance and code patterns for SwiftData persistence in iOS 26+ apps with Swift 6.3, covering @Model definitions, @Attribute and @Relationship options, ModelContainer and ModelContext setup, CRUD operations, @Query in SwiftUI, #Predicate, FetchDescriptor, schema versioning and migration, CloudKit sync, @ModelActor concurrency, and Core Data coexistence. It also lists common mistakes and a review checklist, and points to bundled reference files on advanced topics, queries, predicate pitfalls, indexing, and Core Data coexistence. It produces guidance and review findings rather than executable artifacts.
When to use it
Use it when defining or reviewing SwiftData models, queries, containers, migrations, or CloudKit sync in an iOS app. It also fits work that runs SwiftData alongside an existing Core Data store or migrates screens from Core Data to SwiftData.
Requirements
No scripts; instructions and reference documents only. The described work targets iOS 26+ with Swift 6.3 and assumes an Xcode project; CloudKit sync guidance assumes iCloud and CloudKit entitlements plus remote notifications.

SwiftData

Persist, query, and manage structured data in iOS 26+ apps using SwiftData with Swift 6.3.

Contents

Model Definition

Apply @Model to a class (not struct). It synthesizes PersistentModel conformance. Model instances remain context/actor-bound; pass their PersistentIdentifier, not the instance, across actors.

swift
@Modelclass 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, .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, Dictionary, Set, Codable enums, Codable structs and other compatible Codable value types, and relationships to @Model classes.

ModelContainer Setup

swift
// Basiclet container = try ModelContainer(for: Trip.self, LivingAccommodation.self)
// Configuredlet 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 planlet 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 Sync

ModelConfiguration(..., cloudKitDatabase:) opts a SwiftData store into automatic CloudKit sync, but app entitlements still gate sync.

For any SwiftData CloudKit setup or schema-review task, include a separate Capabilities verdict before schema findings:

  • Capabilities: Xcode target has the iCloud capability with CloudKit enabled and the intended container selected, plus Background Modes > Remote notifications. Without these entitlements, automatic sync is not fully configured even if cloudKitDatabase is set.
  • Schema compatibility: no @Attribute(.unique) or #Unique; relationships are optional, have explicit inverses where needed, and avoid .deny; large Data uses @Attribute(.externalStorage).
  • Scalar attributes: do not make every scalar optional just for CloudKit. Keep required scalars nonoptional when initializers, defaults, or migrations provide valid values.
  • Schema rollout: initialize the development schema only in nonproduction builds, verify it in CloudKit Dashboard, promote before release, and treat production changes as additive only.

CRUD Operations

For destructive batches and migrations, first run the exact predicate or version hop against a disposable copy and record affected identifiers/counts. Execute with explicit transaction/save semantics, refetch, and verify values, relationships, counts, and invariants. On failure, fix the predicate/schema and restore a pristine fixture before retrying; never blindly replay a destructive operation.

swift
// CREATElet trip = Trip(name: "Summer", destination: "Paris", startDate: .now, endDate: .now + 86400*7)modelContext.insert(trip)try modelContext.save()  // or rely on autosave
// READlet trips = try modelContext.fetch(FetchDescriptor<Trip>(    predicate: #Predicate { $0.destination == "Paris" },    sortBy: [SortDescriptor(\.startDate)]))
// UPDATE -- modify properties directly; autosave handles persistencetrip.destination = "Rome"
// DELETEmodelContext.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

swift
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 initstruct 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 querystruct 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

swift
#Predicate<Trip> { $0.destination.localizedStandardContains("paris") }  // Stringlet 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

Supported: ==, !=, <, <=, >, >=, &&, ||, !, contains(), allSatisfy(), filter(), starts(with:), localizedStandardContains(), caseInsensitiveCompare(), arithmetic, conditional expressions, optional chaining and binding, nil coalescing, type casting. Avoid: loops, nested declarations, mutations, and arbitrary unsupported method calls.

FetchDescriptor

swift
var d = FetchDescriptor<Trip>(predicate: ..., sortBy: [...])d.fetchLimit = 20; d.fetchOffset = 0d.includePendingChanges = trued.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 }

Schema Versioning and Migration

swift
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 transformationstatic 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. Verify the stage list covers every supported version hop, then migrate a fresh copy of each old store and assert post-migration data before release.

Core Data Coexistence Boundary

Use this skill when the work is to run SwiftData alongside an existing Core Data store or migrate screens from Core Data to SwiftData over time. Keep pure Core Data stack setup, NSManagedObjectContext, NSFetchRequest, and batch Core Data operations in the sibling core-data skill.

For coexistence, give boundary guidance before detailed migration advice:

  • Point SwiftData and Core Data at the same SQLite store URL.
  • Match Core Data entity names, property names, types, and relationship shapes in the SwiftData @Model definitions.
  • Use @Attribute(originalName:) for SwiftData properties whose persisted Core Data names differ from the Swift names.
  • Do not write the same entity from both stacks at the same time; assign one stack as the writer for each entity during migration.

Concurrency (@ModelActor)

swift
@ModelActoractor 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.

SwiftUI Integration

swift
@mainstruct 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)}

Common Mistakes

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:

swift
// WRONG: await handler.process(trip: trip)// CORRECT: await handler.process(tripID: trip.persistentModelID)

5. ModelContext on wrong actor:

swift
// WRONG: Task.detached { context.fetch(...) }// CORRECT: Use @ModelActor for background work

6. Unsupported #Predicate expressions:

swift
// WRONG: #Predicate<Trip> { $0.name.uppercased() == "PARIS" }// CORRECT: #Predicate<Trip> { $0.name.localizedStandardContains("paris") }

7. Flow control in #Predicate:

swift
// 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:

swift
// 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:

swift
// WRONG: DispatchQueue.global().async { ModelContext(container).fetch(...) }// CORRECT: @ModelActor actor Handler { func fetch() throws { ... } }

Review Checklist

  • Every @Model is a class with a designated initializer
  • All @Transient properties have default values
  • Relationships specify deleteRule and inverse
  • .modelContainer attached at scene/root view level
  • @Query used for reactive data display in SwiftUI
  • #Predicate uses only supported operators
  • Background work uses @ModelActor
  • PersistentIdentifier used across actor boundaries
  • Schema changes have VersionedSchema + SchemaMigrationPlan
  • Large data uses @Attribute(.externalStorage)
  • CloudKit models avoid uniqueness, use optional relationships, avoid .deny, and do not blanket-optionalize scalars
  • CloudKit sync has iCloud + CloudKit, Remote notifications, and production schema rollout checked
  • Explicit save() in @ModelActor methods
  • Previews use ModelConfiguration(isStoredInMemoryOnly: true)
  • @Model classes accessed from SwiftUI views are on @MainActor via @ModelActor or MainActor isolation

References

  • references/swiftdata-advanced.md [blocked] — custom data stores, history tracking, CloudKit, composite attributes, model inheritance, undo/redo, performance
  • references/swiftdata-queries.md [blocked] — @Query variants, FetchDescriptor deep dive, sectioned queries, dynamic queries, background fetch
  • references/core-data-coexistence.md [blocked] — Core Data + SwiftData coexistence and migration boundaries
  • references/predicate-pitfalls.md [blocked] — #Predicate runtime crashes, unsupported expressions, safe patterns
  • references/indexing.md [blocked] — #Index macro, compound indexes, when to index, migration

Source and attribution

Source:dpearson2699/swift-ios-skillsinskills/swiftdataat commit8d90fd1

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

More from dpearson2699/swift-ios-skills

Widgetkit

dpearson2699

Guides implementing, reviewing, and improving WidgetKit widgets and controls for iOS, iPadOS, watchOS, and CarPlay.

Software Development1.1Kupdated 2 months ago

Weatherkit

dpearson2699

Guides iOS developers in fetching WeatherKit forecasts, alerts, and attribution using WeatherService.

Software Development1.1Kupdated 2 months ago

Vision Framework

dpearson2699

Implement computer vision features including text recognition (OCR), face detection, barcode scanning, image segmentation, object tracking, and document scanning in iOS apps. Covers both the modern Swift-native Vision API (iOS 18+) and legacy VNRequest patterns, VisionKit DataScannerViewController for live camera scanning, and CoreMLRequest/VNCoreMLRequest for custom model inference. Use when adding OCR, barcode scanning, face detection, or custom Core ML model inference with Vision.

Awaiting classification1.1Kupdated 2 months ago

Tipkit

dpearson2699

Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.

Awaiting classification1.1Kupdated 2 months ago

Tabletopkit

dpearson2699

Guides building multiplayer spatial board games on visionOS with Apple's TabletopKit and RealityKit.

Software Development1.1Kupdated 2 months ago

Swiftui Webkit

dpearson2699

Guides embedding and controlling web content in SwiftUI apps with WebKit for SwiftUI on iOS 26 and later.

Software Development1.1Kupdated 2 months ago