Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dpearson2699 avatar

Swiftdata

  • 2.9k installs
  • 944 repo stars
  • Updated July 15, 2026
  • dpearson2699/swift-ios-skills

swiftdata is a SwiftUI persistence skill for Model setup, Query and Predicate usage, migrations, CloudKit sync, and ModelActor concurrency on iOS 26+.

About

SwiftData covers persistence, querying, and schema management for iOS 26+ apps using Swift 6.3. It documents Model classes with Attribute, Relationship, Transient, Unique, and Index macros, ModelContainer configuration including in-memory previews and group containers, and CloudKit sync prerequisites with capabilities and schema compatibility verdicts. Query guidance spans Query in SwiftUI, Predicate expressions, FetchDescriptor tuning with prefetch and batch enumeration, VersionedSchema migrations, and ModelActor background work with PersistentIdentifier boundaries. A Core Data coexistence section defines shared SQLite store rules, originalName mapping, and single-writer ownership during migration. Common mistakes call out struct models, missing modelContainer, unsupported Predicate expressions, ObservableObject misuse, and DispatchQueue background fetches. Reference files cover advanced stores, predicate pitfalls, and indexing guidance. Use it when implementing data layers, planning schema migrations, enabling CloudKit sync, or moving screens from Core Data to SwiftData while keeping concurrency safe across actors.

  • Model definition with Attribute, Relationship, Transient, Unique, and Index macros.
  • Query, Predicate, and FetchDescriptor patterns for SwiftUI reactive lists.
  • VersionedSchema and SchemaMigrationPlan for lightweight and custom migrations.
  • CloudKit sync capabilities checklist and schema compatibility constraints.
  • ModelActor concurrency with PersistentIdentifier cross-actor boundaries.

Swiftdata by the numbers

  • 2,889 all-time installs (skills.sh)
  • +127 installs in the week ending Jul 29, 2026 (Skillselion tracking)
  • Ranked #53 of 1,039 Mobile Development skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Jul 31, 2026 (Skillselion catalog sync)
At a glance

swiftdata capabilities & compatibility

Capabilities
model macro configuration and relationship inver · swiftui query and dynamic fetchdescriptor querie · predicate authoring within supported operators · versionedschema lightweight and custom migration · cloudkit sync capability and schema compatibilit · modelactor background processing and identifier · core data coexistence boundary guidance
Use cases
database · frontend
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftdata

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs2.9k
repo stars944
Security audit3 / 3 scanners passed
Last updatedJuly 15, 2026
Repositorydpearson2699/swift-ios-skills

How do I implement SwiftData models, queries, migrations, and CloudKit sync without Predicate crashes or actor boundary bugs?

Define SwiftData models, queries, migrations, CloudKit sync, and ModelActor concurrency for iOS 26+ apps with Swift 6.3.

Who is it for?

iOS developers adding or migrating local persistence with SwiftData and SwiftUI integration.

Skip if: Skip for pure Core Data stack setup; use the sibling core-data skill for NSManagedObject work.

When should I use this skill?

User defines Model classes, Query, Predicate, ModelContainer, migrations, CloudKit sync, or Core Data coexistence.

What you get

Working SwiftData stack with safe queries, migrations, optional CloudKit sync, and background ModelActor imports.

  • @Model schema definitions
  • migration plans
  • query and sync configuration

By the numbers

  • Targets iOS 26+ with Swift 6.3
  • Covers 6+ SwiftData property wrappers including @Model, @Attribute, and @Relationship

Files

SKILL.mdMarkdownGitHub ↗

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). 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, .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

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

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

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

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 }

Schema Versioning and Migration

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.

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)

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

SwiftUI Integration

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

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:

// 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 { ... } }

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

Related skills

How it compares

Pick swiftdata for native SwiftData on modern iOS; use Core Data skills when the app must stay on legacy NSPersistentContainer stacks.

FAQ

Can Model be a struct?

No. Model requires a class with reference semantics and a designated initializer.

How do I pass models across actors?

Pass PersistentIdentifier, not Model instances, across actor boundaries.

What blocks CloudKit sync?

Missing iCloud capability, unique constraints, or non-optional relationships without inverses.

Is Swiftdata safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.