---
title: "Core Data vs SwiftData: When Each Wins"
slug: "core-data-swiftdata-comparison"
category: "ios"
tags: ["ios", "core-data", "swiftdata", "swift", "persistence", "comparison", "migration"]
status: "stable"
last_updated: 2026-08-14
summary: "Decision guide for choosing between Core Data and SwiftData: deployment targets, migration complexity, CloudKit, schema features, and coexistence patterns."
related: ["[[ios/core-data]]", "[[ios/core-data-stack]]", "[[ios/core-data-migrations]]", "[[ios/core-data-fetch-requests]]", "[[ios/swiftui]]", "[[coding/swift]]", "[[ios/core-data-concurrency]]"]
---

## 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 [[ios/core-data]] and [[ios/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.

```swift
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.

```swift
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.

```swift
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 [[ios/core-data-stack]] for the repository pattern.

```swift
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 [[ios/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 [[ios/core-data-concurrency]] for the Core Data patterns that mitigate this.

## Related

- [[ios/core-data]]
- [[ios/core-data-stack]]
- [[ios/core-data-concurrency]]
- [[ios/core-data-migrations]]
- [[ios/core-data-fetch-requests]]
- [[ios/swiftui]]
- [[coding/swift]]
