Every iOS developer with an older app asks the same question: do I rewrite my Core Data layer into SwiftData, or leave what works?
I migrated a production app this year. Here is what actually changes, what breaks, and where I would tell you to draw the line.
Step 1: Know what SwiftData actually is
SwiftData is not a new database. Your data still lives in a SQLite store that Core Data's stack understands. @Model classes compile down to managed objects. This matters for one practical reason: migrating an existing app is possible without losing user data, because the on-disk format is shared.
It also explains the rough edges. When SwiftData behaves oddly (and sometimes it does), the fix is usually a Core Data concept leaking through.
Step 2: Replace NSManagedObject subclasses with @Model macros
The old way: an .xcdatamodeld graph editor, codegen, and a generated class full of optionals:
// Core Data
extension Task {
@NSManaged var title: String
@NSManaged var dueDate: Date?
@NSManaged var isDone: Bool
}The SwiftData way:
@Model
final class Task {
var title: String
var dueDate: Date?
var isDone = false
init(title: String, dueDate: Date? = nil) {
self.title = title
self.dueDate = dueDate
}
}Everything that made Core Data feel foreign disappears: no editor file, no merge-conflicted XML, no force unwraps on generated accessors. Default values are real Swift. Validation is a computed property or a didSet away.
Step 3: Swap the container machinery
Core Data's ceremony:
let container = NSPersistentContainer(name: "Model")
container.loadPersistentStores { _, error in ... }
let context = container.viewContext
// NSFetchedResultsController + delegate dance to update the UISwiftData:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: Task.self)
}
}
struct TaskListView: View {
@Query(filter: #Predicate<Task> { !$0.isDone },
sort: \Task.dueDate)
private var tasks: [Task]
}@Query is the headline. It is NSFetchedResultsController with the delegate boilerplate deleted. Insert, delete, or change a task anywhere and the list updates. No objectWillChange plumbing.
Background writes use @ModelActor instead of newBackgroundContext:
@ModelActor
actor ImportActor {
func importTasks(_ json: Data) throws {
// self.modelContext is isolated to this actor
}
}The import never touches the main thread's context, and you did not have to explain performAndWait to anyone.
Step 4: Handle relationships in Swift
@Model
final class Project {
var name: String
@Relationship(deleteRule: .cascade, inverse: \Task.project)
var tasks: [Task] = []
init(name: String) {
self.name = name
}
}
@Model
final class Task {
var title: String
var project: Project?
}Delete rules live on the property where you can see them. The inverse relationship is explicit. Comparing this to hunting checkboxes in the model editor is not close.
Step 5: Migrate the existing store carefully
The part that decides whether your migration is a weekend or a month:
- Copy a real production database (from a device or a TestFlight build) before writing any code. Synthetic test data lies.
- Try the simple path first. Create
@Modelclasses whose names and property names match your Core Data entities, point aModelContainerat the same store configuration, and run. Lightweight migration often handles it. - If the store opens and counts match, you are done. Do not celebrate yet; check relationships survived by walking a few object graphs.
- Keep
VersionedSchemafrom your first release. The moment SwiftData needs a custom migration plan, you will be glad the schema versions are declared in code:
enum TaskSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Task.self] }
}Where I would draw the line
Migrate if: the app is actively developed, the Core Data layer is small to medium, and you are on iOS 17+ anyway. The deletion of glue code pays for itself within a feature or two.
Do not migrate if: the app is stable and rarely touched, you use heavy NSFetchRequest tuning that SwiftData predicates cannot express yet, or you must support iOS versions below 17. Core Data is not deprecated and is not going anywhere.
Do both if: the app is large. Migrate one feature domain at a time. Both stacks can coexist against the same store file, which lets you ship the migration incrementally instead of betting a release on it.
The honest summary: SwiftData removes most of why people disliked Core Data, and the store compatibility means your users' data survives the switch. That combination made the migration worth it for my app.
Working on an iOS app with a data layer that has grown teeth? I have shipped apps on both stacks and can help you migrate without losing data. Reach out with what you are building.