Why SwiftData (with CloudKit) changed our process
For most of our small apps, SwiftData replaced the entire data layer we used to build. No managed object contexts, no fetch request boilerplate, no manual CloudKit configuration. Here's how we use it in production - and where we still hit walls.
The 80% case
For a typical mk0.net app - local-first, single-user, a handful of models - SwiftData is the right choice. Define your @Model, sprinkle @Query in your views, and you're done. CloudKit sync is one toggle in the container configuration. Backups are free.
The 20% where it bites
- Migrations: SwiftData migrations are still maturing. We treat them like Core Data - version every model change, write migration plans, test on a real device with seeded data.
- Predicates: Complex predicates with relationships sometimes need rewriting as in-memory filters. We accept the trade-off for code clarity.
- CloudKit edge cases: CloudKit zones, account changes, and offline conflicts still require careful UX. We always include a "Re-sync" action and a clear "iCloud sign-in required" state.
Our template defaults
We default to a single ModelContainer at the app root, with CloudKit on for any app that benefits from sync (most do). For local-only apps (book trackers, key vaults), we explicitly disable CloudKit and document the choice in the privacy nutrition label.
Tooling that helps
- Xcode's CloudKit Console for inspecting records during dev
- A debug menu inside the app that exports the current store as JSON
- Snapshot tests of model migrations using a tiny seeded fixture
Verdict
SwiftData is the right default for small SwiftUI apps in 2025. It's not perfect, but it removes enough plumbing that we can ship a working v1 in a week instead of a month. We'll revisit Core Data only when SwiftData clearly can't handle the data shape - and so far, we haven't.