swiftdata

Define, persist, and query structured data in iOS 26+ apps with SwiftData and Swift 6.2. Model definition with @Model classes, @Attribute , @Relationship , @Transient , @Unique , and @Index decorators; supported types include primitives, Date , Data , URL , UUID , Codable enums/structs, and relationships CRUD operations via ModelContext with @Query for reactive SwiftUI views, #Predicate for type-safe filtering, and FetchDescriptor for advanced queries with sorting, limits, and prefetching Schema versioning and migration using VersionedSchema and SchemaMigrationPlan for lightweight or custom data transformations Concurrency support via @ModelActor for background work; pass PersistentIdentifier across actor boundaries, never model objects directly CloudKit sync configuration through ModelConfiguration and coexistence or migration paths from Core Data

INSTALLATION
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swiftdata
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2d

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 (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, Dictionary, Set, Codable enums, Codable structs (composite, iOS 18+), 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))

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

#Predicate<Trip> { $0.startDate > Date.now }                            // Date

#Predicate<Trip> { $0.isFavorite &#x26;&#x26; $0.destination != "Unknown" }       // Compound

#Predicate<Trip> { $0.accommodation?.name != nil }                      // Optional

#Predicate<Trip> { $0.tags.contains { $0.name == "adventure" } }        // Collection

Supported: ==, !=, <, <=, >, >=, &#x26;&#x26;, ||, !, contains(), allSatisfy(), filter(), starts(with:), localizedStandardContains(), caseInsensitiveCompare(), arithmetic, ternary, optional chaining, nil coalescing, type casting. Not supported: flow control, nested declarations, arbitrary 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.

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 use optionals and avoid unique constraints
  • 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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card