Overview
SwiftData (iOS 17+) sits on top of Core Data and exposes a Swift-native API, so the choice is about which API surface you need, not which storage engine. Default to SwiftData for new apps and keep Core Data where the cases below apply. Usage guides: swiftdata and core-data.
Choose SwiftData for new apps targeting iOS 17 or later
An annotated class replaces a model file, a generated subclass, and context plumbing, and @Query keeps views in sync without passing a context around. Additive schema changes migrate automatically, and a versioned schema handles the rest. Schema features have grown: #Index and #Unique, class inheritance (iOS 26), and sectioned queries plus observers outside SwiftUI (iOS 27). Check each feature’s availability against your deployment target.
Stay on Core Data when you ship an existing store
Moving a live store is risky and rarely pays off before a major rewrite. Per-entity NSEntityMigrationPolicy code has no one-to-one SwiftData equivalent; SwiftData offers lightweight and custom migration stages with before and after hooks instead.
Stay on Core Data when the schema needs features SwiftData lacks
SwiftData has no equivalent of fetched properties, derived attributes, or NSOrderedSet-style ordered relationships as far as Apple’s documentation shows; check the current SDK. If any of these is on the roadmap, plan on Core Data.
Know what iOS 27 closed
Sectioned @Query (sectionBy:) covers most of what NSFetchedResultsController’s section key paths did for list screens, and ResultsObserver and HistoryObserver bring fetching and change observation outside SwiftUI views. @Attribute(.codable) persists types you do not own, but those values are opaque to filtering, sorting, and migrations. Re-run the decision if manual sectioning or observation was your only reason for Core Data.
Use Core Data for CloudKit sharing and public databases
NSPersistentCloudKitContainer exposes sharing and the shared and public database scopes. SwiftData’s CloudKit sync covers the private database and requires defaults or optionals on attributes, optional relationships, and no unique constraints. For apps that share records between users, Core Data’s container is the capable option.
Coexist through one SQLite store during an incremental move
Both frameworks can open the same SQLite file, which lets you migrate one subsystem at a time. Follow Apple’s “Adopting SwiftData for a Core Data app” guidance, keep the @Model classes and entities in schema lockstep, give the Swift types distinct names, and enable persistent history tracking on the Core Data side so each stack sees the other’s writes.
let config = ModelConfiguration(url: storeURL)
let container = try ModelContainer(for: Item.self, NewEntity.self, configurations: config)If the long-term choice is unclear, put persistence behind a protocol so views never see either framework.
Pass identifiers, not model objects, across actors
Neither framework’s objects are Sendable: NSManagedObject, NSManagedObjectContext, SwiftData @Model instances, and ModelContext are not, while NSManagedObjectID, PersistentIdentifier, and ModelContainer are. Send the identifier across the actor boundary and re-fetch (model(for:) in a @ModelActor, existingObject(with:) in Core Data). Default MainActor isolation reduces how often this bites in view code; background sync and import code still need it. See swift-concurrency.