Introduction
SwiftData has matured significantly since its introduction at WWDC 2023. With iOS 18 and macOS 15, SwiftData now handles the complex scenarios that kept teams on Core Data: CloudKit sync, efficient batch operations, and reliable migrations. The question has shifted from “Is SwiftData ready?” to “How do we migrate?”
This guide walks through migrating a production Core Data app to SwiftData. We will cover data model conversion, migration strategies that preserve user data, and patterns for running both frameworks during transition.
Why Migrate to SwiftData
Code Reduction
The most immediate benefit is dramatically less boilerplate:
// Core Data: 50+ lines for a basic model
// NSManagedObject subclass + extension + fetch request setup
// SwiftData: 15 lines
@Model
final class Task {
var title: String
var isCompleted: Bool
var dueDate: Date?
var priority: Priority
@Relationship(deleteRule: .cascade) var subtasks: [Subtask]
init(title: String, priority: Priority = .medium) {
self.title = title
self.isCompleted = false
self.priority = priority
self.subtasks = []
}
}
SwiftUI Integration
SwiftData integrates naturally with SwiftUI:
// Query replaces @FetchRequest
@Query(filter: #Predicate<Task> { !$0.isCompleted },
sort: \.dueDate)
var pendingTasks: [Task]
// Changes automatically update views
// No need for NSFetchedResultsController
Type Safety
Predicates are now type-checked at compile time:
// Core Data: Runtime errors possible
let predicate = NSPredicate(format: "tittle == %@", "Test") // Typo undetected
// SwiftData: Compile-time checking
let predicate = #Predicate<Task> { $0.tittle == "Test" } // Compiler error
Assessing Migration Complexity
Before
starting, audit your Core Data usage:
Simple Migration (1-2 weeks)
- Single model container
- Basic relationships (one-to-many)
- No CloudKit sync
- Minimal custom fetch requests
- iOS 17+ deployment target
Moderate Migration (3-4 weeks)
- Multiple entities with relationships
- Background context usage
- Custom migrations
- Basic CloudKit sync
- Need to support iOS 16 (Core Data) alongside iOS 17+ (SwiftData)
Complex Migration (6+ weeks)
- Heavy background processing
- Complex many-to-many relationships
- Derived attributes
- Custom Core Data extensions
- Critical CloudKit sync with conflict resolution
- Must preserve years of user data
Converting Data Models
Ba
sic Model Conversion
Core Data:
// TaskEntity.xcdatamodeld
// Entity: Task
// Attributes:
// - id: UUID
// - title: String
// - isCompleted: Boolean
// - createdAt: Date
// - priority: Integer 16
// NSManagedObject subclass (auto-generated or manual)
extension Task {
@nonobjc public class func fetchRequest() -> NSFetchRequest<Task> {
return NSFetchRequest<Task>(entityName: "Task")
}
@NSManaged public var id: UUID?
@NSManaged public var title: String?
@NSManaged public var isCompleted: Bool
@NSManaged public var createdAt: Date?
@NSManaged public var priority: Int16
}
SwiftData:
import SwiftData
@Model
final class Task {
@Attribute(.unique) var id: UUID
var title: String
var isCompleted: Bool
var createdAt: Date
var priority: Priority
enum Priority: Int, Codable {
case low = 0
case medium = 1
case high = 2
case urgent = 3
}
init(title: String, priority: Priority = .medium) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.createdAt = Date()
self.priority = priority
}
}
Relationship Conversion
Core Data:
// Project Entity
extension Project {
@NSManaged public var id: UUID?
@NSManaged public var name: String?
@NSManaged public var tasks: NSSet? // To-many relationship
}
extension Project {
@objc(addTasksObject:)
@NSManaged public func addToTasks(_ value: Task)
@objc(removeTasksObject:)
@NSManaged public func removeFromTasks(_ value: Task)
}
// Task Entity
extension Task {
@NSManaged public var project: Project? // To-one inverse
}
SwiftData:
@Model
final class Project {
@Attribute(.unique) var id: UUID
var name: String
@Relationship(deleteRule: .cascade, inverse: \Task.project)
var tasks: [Task]
init(name: String) {
self.id = UUID()
self.name = name
self.tasks = []
}
}
@Model
final class Task {
@Attribute(.unique) var id: UUID
var title: String
var isCompleted: Bool
var project: Project?
init(title: String, project: Project? = nil) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.project = project
}
}
Complex Relationship: Many-to-Many
Core Data:
// Tag Entity with many-to-many to Task
extension Tag {
@NSManaged public var id: UUID?
@NSManaged public var name: String?
@NSManaged public var tasks: NSSet?
}
extension Task {
@NSManaged public var tags: NSSet?
}
SwiftData:
@Model
final class Tag {
@Attribute(.unique) var id: UUID
var name: String
var tasks: [Task]
init(name: String) {
self.id = UUID()
self.name = name
self.tasks = []
}
}
@Model
final class Task {
@Attribute(.unique) var id: UUID
var title: String
@Relationship(inverse: \Tag.tasks)
var tags: [Tag]
init(title: String) {
self.id = UUID()
self.title = title
self.tags = []
}
}
Migration Strategies
Strategy 1: Side-by-Side Migration (Recommended)
Run both Core Data and SwiftData simultaneously, migrate data on first launch, then deprecate Core Data.
import SwiftData
import CoreData
class MigrationManager {
static let shared = MigrationManager()
private let userDefaults = UserDefaults.standard
private let migrationKey = "swiftdata_migration_completed"
var needsMigration: Bool {
!userDefaults.bool(forKey: migrationKey) && coreDataStoreExists()
}
private func coreDataStoreExists() -> Bool {
let storeURL = FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)
.first?
.appendingPathComponent("TaskManager.sqlite")
return FileManager.default.fileExists(atPath: storeURL?.path ?? "")
}
func migrateIfNeeded(modelContainer: ModelContainer) async throws {
guard needsMigration else { return }
// Set up Core Data stack for reading
let coreDataStack = CoreDataStack()
// Perform migration
try await performMigration(
from: coreDataStack,
to: modelContainer
)
// Mark migration complete
userDefaults.set(true, forKey: migrationKey)
// Optionally delete Core Data store
// deleteOldCoreDataStore()
}
private func performMigration(
from coreData: CoreDataStack,
to swiftData: ModelContainer
) async throws {
let context = coreData.viewContext
// Fetch all Core Data entities
let projectRequest: NSFetchRequest<CDProject> = CDProject.fetchRequest()
let cdProjects = try context.fetch(projectRequest)
let taskRequest: NSFetchRequest<CDTask> = CDTask.fetchRequest()
let cdTasks = try context.fetch(taskRequest)
let tagRequest: NSFetchRequest<CDTag> = CDTag.fetchRequest()
let cdTags = try context.fetch(tagRequest)
// Migrate in SwiftData context
let swiftDataContext = ModelContext(swiftData)
// First pass: Create all entities without relationships
var projectMap: [UUID: Project] = [:]
var tagMap: [UUID: Tag] = [:]
for cdProject in cdProjects {
guard let id = cdProject.id, let name = cdProject.name else { continue }
let project = Project(name: name)
project.id = id
swiftDataContext.insert(project)
projectMap[id] = project
}
for cdTag in cdTags {
guard let id = cdTag.id, let name = cdTag.name else { continue }
let tag = Tag(name: name)
tag.id = id
swiftDataContext.insert(tag)
tagMap[id] = tag
}
// Second pass: Create tasks with relationships
for cdTask in cdTasks {
guard let id = cdTask.id, let title = cdTask.title else { continue }
let task = Task(title: title)
task.id = id
task.isCompleted = cdTask.isCompleted
task.createdAt = cdTask.createdAt ?? Date()
task.priority = Task.Priority(rawValue: Int(cdTask.priority)) ?? .medium
// Link to project
if let projectId = cdTask.project?.id,
let project = projectMap[projectId] {
task.project = project
}
// Link to tags
if let cdTags = cdTask.tags as? Set<CDTag> {
for cdTag in cdTags {
if let tagId = cdTag.id, let tag = tagMap[tagId] {
task.tags.append(tag)
}
}
}
swiftDataContext.insert(task)
}
try swiftDataContext.save()
}
}
Strategy 2: Fresh Start with Export/Import
For apps where data can be exported and reimported:
struct DataExporter {
struct ExportedData: Codable {
let version: Int
let exportDate: Date
let projects: [ExportedProject]
let tasks: [ExportedTask]
let tags: [ExportedTag]
}
struct ExportedProject: Codable {
let id: UUID
let name: String
}
struct ExportedTask: Codable {
let id: UUID
let title: String
let isCompleted: Bool
let createdAt: Date
let priority: Int
let projectId: UUID?
let tagIds: [UUID]
}
struct ExportedTag: Codable {
let id: UUID
let name: String
}
func exportFromCoreData(context: NSManagedObjectContext) throws -> ExportedData {
// Fetch and convert all entities
let projectRequest: NSFetchRequest<CDProject> = CDProject.fetchRequest()
let projects = try context.fetch(projectRequest).compactMap { cdProject -> ExportedProject? in
guard let id = cdProject.id, let name = cdProject.name else { return nil }
return ExportedProject(id: id, name: name)
}
let taskRequest: NSFetchRequest<CDTask> = CDTask.fetchRequest()
let tasks = try context.fetch(taskRequest).compactMap { cdTask -> ExportedTask? in
guard let id = cdTask.id, let title = cdTask.title else { return nil }
let tagIds = (cdTask.tags as? Set<CDTag>)?.compactMap { $0.id } ?? []
return ExportedTask(
id: id,
title: title,
isCompleted: cdTask.isCompleted,
createdAt: cdTask.createdAt ?? Date(),
priority: Int(cdTask.priority),
projectId: cdTask.project?.id,
tagIds: tagIds
)
}
let tagRequest: NSFetchRequest<CDTag> = CDTag.fetchRequest()
let tags = try context.fetch(tagRequest).compactMap { cdTag -> ExportedTag? in
guard let id = cdTag.id, let name = cdTag.name else { return nil }
return ExportedTag(id: id, name: name)
}
return ExportedData(
version: 1,
exportDate: Date(),
projects: projects,
tasks: tasks,
tags: tags
)
}
func importToSwiftData(_ data: ExportedData, context: ModelContext) throws {
var projectMap: [UUID: Project] = [:]
var tagMap: [UUID: Tag] = [:]
// Import projects
for exported in data.projects {
let project = Project(name: exported.name)
project.id = exported.id
context.insert(project)
projectMap[exported.id] = project
}
// Import tags
for exported in data.tags {
let tag = Tag(name: exported.name)
tag.id = exported.id
context.insert(tag)
tagMap[exported.id] = tag
}
// Import tasks
for exported in data.tasks {
let task = Task(title: exported.title)
task.id = exported.id
task.isCompleted = exported.isCompleted
task.createdAt = exported.createdAt
task.priority = Task.Priority(rawValue: exported.priority) ?? .medium
if let projectId = exported.projectId {
task.project = projectMap[projectId]
}
for tagId in exported.tagIds {
if let tag = tagMap[tagId] {
task.tags.append(tag)
}
}
context.insert(task)
}
try context.save()
}
}
Strategy 3: Gradual Feature Migration
Migrate feature by feature while maintaining Core Data for existing data:
// DataManager that abstracts storage backend
protocol TaskStorage {
func fetchTasks() async throws -> [TaskDTO]
func save(_ task: TaskDTO) async throws
func delete(_ task: TaskDTO) async throws
}
struct TaskDTO {
let id: UUID
var title: String
var isCompleted: Bool
var createdAt: Date
var priority: Int
}
// Core Data implementation
class CoreDataTaskStorage: TaskStorage {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
func fetchTasks() async throws -> [TaskDTO] {
try await context.perform {
let request: NSFetchRequest<CDTask> = CDTask.fetchRequest()
let results = try self.context.fetch(request)
return results.compactMap { cdTask in
guard let id = cdTask.id, let title = cdTask.title else { return nil }
return TaskDTO(
id: id,
title: title,
isCompleted: cdTask.isCompleted,
createdAt: cdTask.createdAt ?? Date(),
priority: Int(cdTask.priority)
)
}
}
}
func save(_ task: TaskDTO) async throws {
try await context.perform {
let request: NSFetchRequest<CDTask> = CDTask.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", task.id as CVarArg)
let existing = try self.context.fetch(request).first
let cdTask = existing ?? CDTask(context: self.context)
cdTask.id = task.id
cdTask.title = task.title
cdTask.isCompleted = task.isCompleted
cdTask.createdAt = task.createdAt
cdTask.priority = Int16(task.priority)
try self.context.save()
}
}
func delete(_ task: TaskDTO) async throws {
try await context.perform {
let request: NSFetchRequest<CDTask> = CDTask.fetchRequest()
request.predicate = NSPredicate(format: "id == %@", task.id as CVarArg)
if let cdTask = try self.context.fetch(request).first {
self.context.delete(cdTask)
try self.context.save()
}
}
}
}
// SwiftData implementation
@MainActor
class SwiftDataTaskStorage: TaskStorage {
private let modelContext: ModelContext
init(modelContext: ModelContext) {
self.modelContext = modelContext
}
func fetchTasks() async throws -> [TaskDTO] {
let descriptor = FetchDescriptor<Task>(sortBy: [SortDescriptor(\.createdAt)])
let tasks = try modelContext.fetch(descriptor)
return tasks.map { task in
TaskDTO(
id: task.id,
title: task.title,
isCompleted: task.isCompleted,
createdAt: task.createdAt,
priority: task.priority.rawValue
)
}
}
func save(_ task: TaskDTO) async throws {
let predicate = #Predicate<Task> { $0.id == task.id }
var descriptor = FetchDescriptor(predicate: predicate)
descriptor.fetchLimit = 1
let existing = try modelContext.fetch(descriptor).first
if let swiftTask = existing {
swiftTask.title = task.title
swiftTask.isCompleted = task.isCompleted
swiftTask.priority = Task.Priority(rawValue: task.priority) ?? .medium
} else {
let swiftTask = Task(title: task.title)
swiftTask.id = task.id
swiftTask.isCompleted = task.isCompleted
swiftTask.createdAt = task.createdAt
swiftTask.priority = Task.Priority(rawValue: task.priority) ?? .medium
modelContext.insert(swiftTask)
}
try modelContext.save()
}
func delete(_ task: TaskDTO) async throws {
let predicate = #Predicate<Task> { $0.id == task.id }
var descriptor = FetchDescriptor(predicate: predicate)
descriptor.fetchLimit = 1
if let swiftTask = try modelContext.fetch(descriptor).first {
modelContext.delete(swiftTask)
try modelContext.save()
}
}
}
// Feature flag to switch storage
class StorageFactory {
static func createTaskStorage() -> TaskStorage {
if FeatureFlags.useSwiftData {
let container = try! ModelContainer(for: Task.self)
return SwiftDataTaskStorage(modelContext: container.mainContext)
} else {
let coreDataStack = CoreDataStack.shared
return CoreDataTaskStorage(context: coreDataStack.viewContext)
}
}
}
Handling Schema Changes
SwiftData Schema Versioning
// Define schema versions
enum TaskSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self, Project.self]
}
@Model
final class Task {
var id: UUID
var title: String
var isCompleted: Bool
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
}
}
@Model
final class Project {
var id: UUID
var name: String
var tasks: [Task]
init(name: String) {
self.id = UUID()
self.name = name
self.tasks = []
}
}
}
// Version 2: Added priority and due date
enum TaskSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] {
[Task.self, Project.self]
}
@Model
final class Task {
var id: UUID
var title: String
var isCompleted: Bool
var priority: Int // New field
var dueDate: Date? // New field
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.priority = 1 // Default to medium
self.dueDate = nil
}
}
@Model
final class Project {
var id: UUID
var name: String
var tasks: [Task]
init(name: String) {
self.id = UUID()
self.name = name
self.tasks = []
}
}
}
Migration Plans
enum TaskMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[TaskSchemaV1.self, TaskSchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.custom(
fromVersion: TaskSchemaV1.self,
toVersion: TaskSchemaV2.self
) { context in
// Custom migration logic
let tasks = try context.fetch(FetchDescriptor<TaskSchemaV2.Task>())
for task in tasks {
// Set default values for new fields
if task.priority == 0 {
task.priority = 1 // Medium priority
}
// dueDate defaults to nil, which is fine
}
try context.save()
}
}
// Configure container with migration plan
let container = try ModelContainer(
for: Task.self, Project.self,
migrationPlan: TaskMigrationPlan.self
)
CloudKit Sync Considerations
Migrating CloudKit-Synced Data
// SwiftData with CloudKit
let schema = Schema([Task.self, Project.self, Tag.self])
let configuration = ModelConfiguration(
"TaskManager",
schema: schema,
cloudKitDatabase: .private("iCloud.com.yourapp.taskmanager")
)
let container = try ModelContainer(
for: schema,
configurations: [configuration]
)
// Important: CloudKit sync happens automatically
// But you need to handle potential conflicts
Conflict Resolution
@Model
final class Task {
@Attribute(.unique) var id: UUID
var title: String
var isCompleted: Bool
var lastModified: Date // Track for conflict resolution
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.lastModified = Date()
}
func update(title: String? = nil, isCompleted: Bool? = nil) {
if let title = title {
self.title = title
}
if let isCompleted = isCompleted {
self.isCompleted = isCompleted
}
self.lastModified = Date()
}
}
// Monitor sync status
class CloudKitSyncMonitor: ObservableObject {
@Published var syncStatus: SyncStatus = .idle
enum SyncStatus {
case idle
case syncing
case error(Error)
case upToDate
}
init() {
observeCloudKitNotifications()
}
private func observeCloudKitNotifications() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleImport),
name: NSPersistentCloudKitContainer.eventChangedNotification,
object: nil
)
}
@objc private func handleImport(_ notification: Notification) {
guard let event = notification.userInfo?[
NSPersistentCloudKitContainer.eventNotificationUserInfoKey
] as? NSPersistentCloudKitContainer.Event else { return }
DispatchQueue.main.async {
switch event.type {
case .setup:
self.syncStatus = .syncing
case .import:
if event.succeeded {
self.syncStatus = .upToDate
} else if let error = event.error {
self.syncStatus = .error(error)
}
case .export:
if event.succeeded {
self.syncStatus = .upToDate
}
@unknown default:
break
}
}
}
}
Testing the Migration
Unit Tests for Migration Logic
import XCTest
import CoreData
@testable import YourApp
final class MigrationTests: XCTestCase {
var coreDataStack: CoreDataStack!
var swiftDataContainer: ModelContainer!
override func setUp() async throws {
// Set up in-memory Core Data stack
coreDataStack = CoreDataStack(inMemory: true)
// Set up in-memory SwiftData container
let config = ModelConfiguration(isStoredInMemoryOnly: true)
swiftDataContainer = try ModelContainer(
for: Task.self, Project.self, Tag.self,
configurations: [config]
)
}
override func tearDown() async throws {
coreDataStack = nil
swiftDataContainer = nil
}
func testBasicTaskMigration() async throws {
// Arrange: Create Core Data tasks
let context = coreDataStack.viewContext
let cdTask = CDTask(context: context)
cdTask.id = UUID()
cdTask.title = "Test Task"
cdTask.isCompleted = false
cdTask.createdAt = Date()
cdTask.priority = 2
try context.save()
// Act: Migrate
let migrationManager = MigrationManager()
try await migrationManager.performMigration(
from: coreDataStack,
to: swiftDataContainer
)
// Assert: Verify SwiftData contains migrated data
let modelContext = ModelContext(swiftDataContainer)
let descriptor = FetchDescriptor<Task>()
let tasks = try modelContext.fetch(descriptor)
XCTAssertEqual(tasks.count, 1)
XCTAssertEqual(tasks.first?.title, "Test Task")
XCTAssertEqual(tasks.first?.priority, .high)
}
func testProjectTaskRelationshipMigration() async throws {
// Arrange: Create project with tasks in Core Data
let context = coreDataStack.viewContext
let cdProject = CDProject(context: context)
cdProject.id = UUID()
cdProject.name = "Test Project"
let cdTask1 = CDTask(context: context)
cdTask1.id = UUID()
cdTask1.title = "Task 1"
cdTask1.project = cdProject
let cdTask2 = CDTask(context: context)
cdTask2.id = UUID()
cdTask2.title = "Task 2"
cdTask2.project = cdProject
try context.save()
// Act: Migrate
let migrationManager = MigrationManager()
try await migrationManager.performMigration(
from: coreDataStack,
to: swiftDataContainer
)
// Assert: Verify relationships are preserved
let modelContext = ModelContext(swiftDataContainer)
let projectDescriptor = FetchDescriptor<Project>()
let projects = try modelContext.fetch(projectDescriptor)
XCTAssertEqual(projects.count, 1)
XCTAssertEqual(projects.first?.tasks.count, 2)
}
func testMigrationPreservesUUIDs() async throws {
// Arrange
let context = coreDataStack.viewContext
let originalId = UUID()
let cdTask = CDTask(context: context)
cdTask.id = originalId
cdTask.title = "Test Task"
try context.save()
// Act
let migrationManager = MigrationManager()
try await migrationManager.performMigration(
from: coreDataStack,
to: swiftDataContainer
)
// Assert: UUID is preserved
let modelContext = ModelContext(swiftDataContainer)
let predicate = #Predicate<Task> { $0.id == originalId }
let descriptor = FetchDescriptor(predicate: predicate)
let tasks = try modelContext.fetch(descriptor)
XCTAssertEqual(tasks.count, 1)
XCTAssertEqual(tasks.first?.id, originalId)
}
func testLargeMigrationPerformance() async throws {
// Arrange: Create 10,000 tasks
let context = coreDataStack.viewContext
for i in 0..less than 10_000 {
let cdTask = CDTask(context: context)
cdTask.id = UUID()
cdTask.title = "Task \(i)"
cdTask.isCompleted = i % 2 == 0
cdTask.createdAt = Date()
cdTask.priority = Int16(i % 4)
}
try context.save()
// Act & Assert: Migration completes in reasonable time
let migrationManager = MigrationManager()
let start = CFAbsoluteTimeGetCurrent()
try await migrationManager.performMigration(
from: coreDataStack,
to: swiftDataContainer
)
let duration = CFAbsoluteTimeGetCurrent() - start
XCTAssertLessThan(duration, 30.0, "Migration should complete within 30 seconds")
// Verify count
let modelContext = ModelContext(swiftDataContainer)
let descriptor = FetchDescriptor<Task>()
let tasks = try modelContext.fetch(descriptor)
XCTAssertEqual(tasks.count, 10_000)
}
}
Production Migration Checklist
Pre-Migration
-
Backup Strategy
- Implement automatic backup before migration
- Store backup in separate location
- Test backup restoration
-
Feature Flags
- Add flag to enable/disable migration
- Ability to rollback to Core Data if needed
-
Analytics
- Track migration start/completion
- Log any errors with context
- Measure migration duration
During Migration
class ProductionMigrationManager {
func performMigration() async {
// 1. Track migration start
Analytics.track("migration_started", properties: [
"core_data_version": coreDataVersion,
"task_count": taskCount,
"project_count": projectCount
])
// 2. Create backup
do {
try await createBackup()
} catch {
Analytics.track("migration_backup_failed", properties: ["error": error.localizedDescription])
return // Don't proceed without backup
}
// 3. Perform migration
let startTime = CFAbsoluteTimeGetCurrent()
do {
try await performMigrationInternal()
let duration = CFAbsoluteTimeGetCurrent() - startTime
Analytics.track("migration_completed", properties: [
"duration_seconds": duration,
"tasks_migrated": migratedTaskCount
])
} catch {
Analytics.track("migration_failed", properties: [
"error": error.localizedDescription,
"duration_seconds": CFAbsoluteTimeGetCurrent() - startTime
])
// Attempt rollback
await attemptRollback()
}
}
private func createBackup() async throws {
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
let backupURL = documentsURL.appendingPathComponent("CoreDataBackup")
// Copy entire Core Data store
let storeURL = documentsURL.appendingPathComponent("TaskManager.sqlite")
if fileManager.fileExists(atPath: backupURL.path) {
try fileManager.removeItem(at: backupURL)
}
try fileManager.createDirectory(at: backupURL, withIntermediateDirectories: true)
// Copy all store files
let storeFiles = ["TaskManager.sqlite", "TaskManager.sqlite-shm", "TaskManager.sqlite-wal"]
for file in storeFiles {
let sourceURL = documentsURL.appendingPathComponent(file)
let destURL = backupURL.appendingPathComponent(file)
if fileManager.fileExists(atPath: sourceURL.path) {
try fileManager.copyItem(at: sourceURL, to: destURL)
}
}
}
}
Post-Migration
-
Verification
- Compare record counts
- Spot-check relationships
- Verify CloudKit sync (if applicable)
-
Cleanup
- Delete Core Data store after confirmed success
- Remove migration code after full rollout
- Archive backup after stability period
Conclusion
Migrating from Core Data to SwiftData requires careful planning but delivers significant long-term benefits: cleaner code, better SwiftUI integration, and compile-time safety. The side-by-side migration strategy provides the safest path forward, allowing you to validate the migration thoroughly before cutting over.
Start with a comprehensive audit of your Core Data usage. Plan for the complexity level that matches your app. Test extensively, especially relationship preservation and edge cases. And always maintain a rollback path until the migration proves stable in production.
The investment in migration pays dividends every time you write a query, add a model property, or debug a data issue. SwiftData is the future of iOS data persistence, and now is the right time to make the transition.
Planning a Core Data to SwiftData migration? We have migrated multiple production apps with millions of records. Contact us for a migration assessment and strategy session.