Overview

SwiftData (iOS 17+) is Core Data with a Swift-native surface: @Model replaces the data model editor, @Query replaces @FetchRequest, and automatic migrations handle additive schema changes. Core Data is the battle-tested stack with migration tooling, CloudKit configuration knobs, and derived attributes that SwiftData does not yet match. Neither is universally superior; the right choice depends on deployment target, schema complexity, and whether the codebase already ships a store. This page is a companion to core-data and core-data-stack.

Use SwiftData for new apps targeting iOS 17+ with simple schemas

SwiftData removes boilerplate. A model that would take an .xcdatamodeld file, a generated subclass, and a context setup becomes three lines.

import SwiftData
 
@Model
final class Item {
  var title: String
  var createdAt: Date
  var isArchived: Bool = false
 
  init(title: String) {
    self.title = title
    self.createdAt = Date()
  }
}

Additive schema changes (new attribute, new model) migrate automatically. @Query in a view reacts to changes without a context injection. The gain in code clarity is substantial on greenfield apps.

Stay on Core Data when you ship an existing store

Migrating a live Core Data store to SwiftData is not trivial. The stores use the same SQLite file format, and SwiftData can read a Core Data store, but the migration path requires care: attribute naming conventions differ, and any custom NSEntityMigrationPolicy has no SwiftData equivalent. For existing apps, the cost of migration rarely pays off until a major rewrite.

Stay on Core Data when the schema uses features SwiftData does not support

As of iOS 26, and unchanged by the WWDC 2026 SwiftData additions, SwiftData does not support:

  • Fetched properties (computed cross-entity lookups).
  • Derived attributes (aggregate values stored at save time).
  • Ordered relationships backed by NSOrderedSet.
  • Custom NSEntityMigrationPolicy for complex multi-version data transforms.

If any of these are on the roadmap, stay on Core Data.

WWDC 2026 narrowed, but did not close, the historic gaps

SwiftData’s 2026 additions do not touch the list above, but they remove three long-standing reasons to reach for Core Data. @Query(sort:, sectionBy:) groups fetched results into sections without hand-rolled partitioning, closing most of the gap with NSFetchedResultsController’s sectionNameKeyPath for simple list screens. @Attribute(.codable) persists a type you don’t own (an MKMapItem.Identifier, a third-party DTO) via Codable serialization; the attribute becomes opaque to predicates and sort descriptors, so keep typed attributes for anything you filter or sort on. ResultsObserver and HistoryObserver bring @Query-style fetching and change observation to code outside SwiftUI views, which previously meant wrapping an NSFetchedResultsController or hand-writing a Combine pipeline.

import SwiftData
 
@Model
final class Trip {
  var destination: String
  var startDate: Date
}
 
@Query(sort: \Trip.startDate, sectionBy: \.destination)
var trips: [Trip]
 
// In the view body:
ForEach(_trips.sections) { section in
  Section(section.id) {
    ForEach(section) { trip in TripRow(trip: trip) }
  }
}

Re-run the decision above per app: if the only reason you were on Core Data was manual sectioning or ad hoc observation outside SwiftUI, SwiftData is now a closer call.

Use Core Data for NSPersistentCloudKitContainer with fine-grained CloudKit control

NSPersistentCloudKitContainer lets you configure record zones, sharing, and sync conflict policies directly. SwiftData has CloudKit sync support, but the configuration surface is smaller. For apps that rely on CloudKit sharing (share a list with another user, conflict resolution), Core Data’s container is more capable today.

Coexist using a shared SQLite store during incremental migration

SwiftData and Core Data can share the same SQLite file. This allows you to migrate one entity or subsystem at a time rather than all at once.

let storeURL = PersistenceController.shared.container.persistentStoreDescriptions
  .first!.url!
let config = ModelConfiguration(url: storeURL)
let container = try ModelContainer(
  for: Item.self, NewEntity.self,
  configurations: config
)

Test the shared store approach thoroughly. Both frameworks write to the same file; conflicts arise when both write the same row in the same transaction.

Abstract persistence behind a protocol to stay framework-agnostic

If you are uncertain about the long-term choice, wrap persistence in a protocol from day one. Both a Core Data implementation and a SwiftData implementation satisfy the same interface; you swap one for the other without touching view code. See core-data-stack for the repository pattern.

protocol NoteRepository {
  func notes() throws -> [Note]
  func add(title: String, body: String) throws
  func delete(id: UUID) throws
}

Concrete types CoreDataNoteRepository and SwiftDataNoteRepository implement the protocol independently. The swiftui view layer receives the protocol type.

Prefer SwiftData for Swift 6 strict concurrency compliance

SwiftData’s @Model objects are Sendable under the actor model; NSManagedObject is not. In a Swift 6 codebase with strict concurrency checking, passing NSManagedObject across actor boundaries requires unsafe annotations or careful NSManagedObjectID plumbing. Swift 6.2’s approachable concurrency (default MainActor isolation in new Xcode 26+ projects) reduces how often this bites in view-adjacent code, since most of it already runs on the main actor; the difference still matters for background sync and import code that explicitly opts out of main-actor isolation. For teams adopting Swift 6 on new code, SwiftData remains easier to keep warning-free. See core-data-concurrency for the Core Data patterns that mitigate this.