SwiftData vs Core Data: Migration Guide for iOS Persistence in 2026

SwiftData Core Data Migration for iOS

SwiftData Core Data migration is one of the most impactful modernization steps for iOS developers in 2026. SwiftData, introduced at WWDC 2023 and now mature in iOS 18+, replaces the verbose and error-prone Core Data API with a declarative, Swift-native persistence framework. It uses macros, property wrappers, and Swift concurrency to deliver a dramatically simpler developer experience while maintaining Core Data’s powerful storage engine underneath.

This guide provides a practical migration path from Core Data to SwiftData, covering model conversion, query syntax, relationships, CloudKit integration, and strategies for incremental adoption in existing applications.

Core Data vs SwiftData: What Changes

Core Data vs SwiftData Comparison

┌────────────────────────┬───────────────────┬───────────────────┐
│ Feature                │ Core Data         │ SwiftData         │
├────────────────────────┼───────────────────┼───────────────────┤
│ Model Definition       │ .xcdatamodeld     │ Swift classes     │
│ Schema                 │ Visual editor     │ @Model macro      │
│ Fetch Requests         │ NSFetchRequest    │ #Predicate macro  │
│ Context                │ NSManagedObject   │ ModelContext       │
│                        │ Context           │                   │
│ Concurrency            │ Manual (perform)  │ @ModelActor       │
│ Relationships          │ Visual + code     │ Swift properties  │
│ CloudKit               │ NSPersistentCloud │ Built-in          │
│                        │ KitContainer      │                   │
│ Undo Support           │ Manual            │ Automatic         │
│ Migration              │ Mapping models    │ VersionedSchema   │
│ SwiftUI Integration    │ @FetchRequest     │ @Query            │
└────────────────────────┴───────────────────┴───────────────────┘
SwiftData Core Data iOS persistence comparison
SwiftData simplifies iOS persistence with Swift-native model definitions and queries

Converting Core Data Models to SwiftData

The first migration step is converting your Core Data entity definitions to SwiftData model classes. Moreover, SwiftData uses the @Model macro to generate all the persistence boilerplate:

// BEFORE: Core Data NSManagedObject subclass
// (plus .xcdatamodeld visual model file)
class CDTask: NSManagedObject {
    @NSManaged var id: UUID
    @NSManaged var title: String
    @NSManaged var notes: String?
    @NSManaged var isCompleted: Bool
    @NSManaged var dueDate: Date?
    @NSManaged var createdAt: Date
    @NSManaged var priority: Int16
    @NSManaged var category: CDCategory?
    @NSManaged var tags: NSSet?
}

// AFTER: SwiftData @Model class
// No .xcdatamodeld file needed!
@Model
final class Task {
    var id: UUID
    var title: String
    var notes: String?
    var isCompleted: Bool
    var dueDate: Date?
    var createdAt: Date
    var priority: Int

    // Relationships are just Swift properties
    var category: Category?

    @Relationship(deleteRule: .nullify, inverse: \Tag.tasks)
    var tags: [Tag]

    // Transient properties (not persisted)
    @Transient var isOverdue: Bool {
        guard let dueDate else { return false }
        return !isCompleted && dueDate < Date()
    }

    init(title: String, priority: Int = 0) {
        self.id = UUID()
        self.title = title
        self.isCompleted = false
        self.createdAt = Date()
        self.priority = priority
        self.tags = []
    }
}

@Model
final class Category {
    var name: String
    var colorHex: String

    @Relationship(deleteRule: .cascade, inverse: \Task.category)
    var tasks: [Task]

    init(name: String, colorHex: String) {
        self.name = name
        self.colorHex = colorHex
        self.tasks = []
    }
}

Query Migration: NSFetchRequest to #Predicate

SwiftData replaces NSFetchRequest with type-safe Swift predicates. Therefore, your queries are checked at compile time instead of failing at runtime:

// BEFORE: Core Data fetch request
let request = NSFetchRequest<CDTask>(entityName: "CDTask")
request.predicate = NSPredicate(
    format: "isCompleted == %@ AND priority >= %d AND category.name == %@",
    NSNumber(value: false), 2, "Work"
)
request.sortDescriptors = [
    NSSortDescriptor(key: "dueDate", ascending: true),
    NSSortDescriptor(key: "priority", ascending: false)
]
request.fetchLimit = 20
let results = try context.fetch(request)

// AFTER: SwiftData query with #Predicate
let isCompleted = false
let minPriority = 2
let categoryName = "Work"

let descriptor = FetchDescriptor<Task>(
    predicate: #Predicate {
        $0.isCompleted == isCompleted &&
        $0.priority >= minPriority &&
        $0.category?.name == categoryName
    },
    sortBy: [
        SortDescriptor(\Task.dueDate),
        SortDescriptor(\Task.priority, order: .reverse)
    ]
)
descriptor.fetchLimit = 20
let results = try modelContext.fetch(descriptor)

SwiftUI Integration with @Query

// BEFORE: Core Data @FetchRequest in SwiftUI
struct TaskListView: View {
    @FetchRequest(
        sortDescriptors: [SortDescriptor(\CDTask.dueDate)],
        predicate: NSPredicate(format: "isCompleted == false")
    ) var tasks: FetchedResults<CDTask>

    var body: some View {
        List(tasks) { task in
            TaskRow(task: task)
        }
    }
}

// AFTER: SwiftData @Query in SwiftUI
struct TaskListView: View {
    @Query(
        filter: #Predicate<Task> { !$0.isCompleted },
        sort: \Task.dueDate
    ) var tasks: [Task]

    @Environment(\.modelContext) var context

    var body: some View {
        List(tasks) { task in
            TaskRow(task: task)
        }
        .swipeActions {
            Button("Delete") {
                context.delete(task)
            }
        }
    }
}
SwiftUI integration with SwiftData query macro
SwiftData integrates seamlessly with SwiftUI through the @Query property wrapper

Schema Migration with VersionedSchema

SwiftData handles schema evolution through VersionedSchema and SchemaMigrationPlan. Additionally, this approach is safer than Core Data mapping models because migrations are defined in Swift code:

enum TaskSchemaV1: VersionedSchema {
    static var versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Task.self, Category.self]
    }

    @Model final class Task {
        var id: UUID
        var title: String
        var isCompleted: Bool
        var createdAt: Date
        init(title: String) {
            self.id = UUID()
            self.title = title
            self.isCompleted = false
            self.createdAt = Date()
        }
    }
}

enum TaskSchemaV2: VersionedSchema {
    static var versionIdentifier = Schema.Version(2, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Task.self, Category.self, Tag.self]
    }

    @Model final class Task {
        var id: UUID
        var title: String
        var isCompleted: Bool
        var createdAt: Date
        var priority: Int  // NEW field
        var tags: [Tag]    // NEW relationship
        init(title: String) {
            self.id = UUID()
            self.title = title
            self.isCompleted = false
            self.createdAt = Date()
            self.priority = 0
            self.tags = []
        }
    }
}

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
        // Set default priority for existing tasks
        let tasks = try context.fetch(
            FetchDescriptor<TaskSchemaV2.Task>())
        for task in tasks {
            task.priority = 1
        }
        try context.save()
    }
}

When NOT to Use SwiftData

SwiftData requires iOS 17+ minimum deployment target. If your app supports iOS 15 or 16, you must continue using Core Data. Furthermore, SwiftData does not yet support all Core Data features — complex fetch request templates, abstract entities, and some advanced CloudKit configurations may still require Core Data. If your app has a heavily battle-tested Core Data stack with extensive migration history, the risk of introducing bugs during migration may outweigh the developer experience benefits. Consequently, consider a gradual migration where new features use SwiftData while existing features remain on Core Data.

iOS app development with SwiftData persistence
Plan your migration timeline based on minimum deployment target and feature requirements

Key Takeaways

  • SwiftData Core Data migration replaces verbose Objective-C patterns with declarative Swift-native persistence
  • The @Model macro eliminates .xcdatamodeld files and NSManagedObject subclasses
  • Type-safe #Predicate queries catch errors at compile time instead of runtime
  • VersionedSchema provides safe, code-based schema migration without mapping models
  • Adopt incrementally — SwiftData and Core Data can coexist in the same application during migration

Related Reading

External Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top