---
name: ios-swiftdata-modern
description: "SwiftData (iOS 17+) the production way — @Model, @Query, CloudKit sync, predicates, migrations. Builds SwiftData stacks for iOS 17+: @Model schema design, @Query with predicates and sort descriptors, ModelContext patterns, CloudKit sync, schema migrations, and the SwiftData vs Core Data decision tree for new apps."
license: MIT
---

# iOS SwiftData Modern

A focused skill for building persistence layers with SwiftData, Apple's declarative framework introduced at WWDC23 for iOS 17+. It helps you design `@Model` schemas, write type-safe `#Predicate` queries, wire up `ModelContainer`/`ModelContext`, enable CloudKit mirroring, and plan schema migrations — without falling back into Core Data ceremony. Aimed at iOS developers starting new apps or moving off `NSManagedObject` boilerplate.

## When to use it

- You're starting a fresh iOS 17+ (or iOS 18) app and want persistence wired correctly the first time.
- You need `@Query` to drive a SwiftUI list with filtering and sorting that stays in sync automatically.
- You want iCloud sync across a user's devices and aren't sure how `automatic` CloudKit configuration constrains your model.
- You have a shipped schema and need to evolve it safely with a `SchemaMigrationPlan`.
- You're weighing SwiftData against Core Data and want a clear decision tree rather than vibes.

## How it works

The skill scaffolds the real building blocks:

- **Schema** — classes annotated with `@Model`, using `@Attribute(.unique)`, `@Attribute(.externalStorage)` for blobs, `@Relationship(deleteRule: .cascade)`, and `@Transient` for non-persisted fields.
- **Container** — `.modelContainer(for:)` on your `App` scene, or a manually built `ModelContainer(for:configurations:)` with a `ModelConfiguration` (including `.modelContainer(... isStoredInMemoryOnly:)` for tests/previews).
- **Queries** — `@Query(filter:sort:)` with `#Predicate` and `SortDescriptor`, plus `FetchDescriptor` and `context.fetch(_:)` for imperative reads.
- **Writes** — `@Environment(\.modelContext)`, then `context.insert(_:)`, `context.delete(_:)`, and `context.save()`.
- **Sync** — a CloudKit-backed `ModelConfiguration(cloudKitDatabase:)`, with the constraints it imposes (no `.unique`, all properties optional or defaulted, no `deleteRule: .deny`).
- **Migrations** — `VersionedSchema` types plus a `SchemaMigrationPlan` with lightweight and `.custom` migration stages.

## Usage example

```swift
@Model
final class Trip {
    var name: String
    var startDate: Date
    @Relationship(deleteRule: .cascade) var stops: [Stop] = []
    init(name: String, startDate: Date) { self.name = name; self.startDate = startDate }
}

// In a SwiftUI view
@Query(filter: #Predicate<Trip> { $0.startDate > .now },
       sort: \Trip.startDate) private var upcoming: [Trip]
@Environment(\.modelContext) private var context
```

## Tips & gotchas

- `#Predicate` runs against the store, so unsupported operations (many `String` methods, computed properties) throw at fetch time, not compile time — keep predicates simple.
- CloudKit mirroring forbids `@Attribute(.unique)` and requires every property to be optional or have a default; design for that from day one or migration is painful.
- `@Query` only refreshes for the context it's attached to — background writes need the same container, and you may need `context.save()` for cross-context visibility.
- Use `isStoredInMemoryOnly: true` configurations in `#Preview` and unit tests to avoid polluting the on-disk store.

## FAQ

**Should I pick SwiftData or Core Data?** For new iOS 17+ apps, SwiftData unless you need NSFetchedResultsController-level control, fine-grained migration, or pre-iOS-17 support — then Core Data (or both, since they share the store).

**Can I run raw fetches outside SwiftUI?** Yes — build a `FetchDescriptor` with a `#Predicate` and call `modelContext.fetch(_:)`.

**Do I get migrations for free?** Additive changes are lightweight automatically; renames or type changes need a `SchemaMigrationPlan` with a custom stage.
