# Migratie van Core Data naar SwiftData: Stap-voor-stap-gids 2026 > Volledige gids om een iOS-app te migreren van Core Data naar SwiftData met praktische voorbeelden, coexistence-strategieën en best practices. - Published: 2026-02-27 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftdata, core-data, ios, migration, swift - Reading time: 16 min --- SwiftData vertegenwoordigt de toekomst van data-persistentie op Apple-platforms. Geïntroduceerd op WWDC 2023 biedt dit framework een native Swift-syntax en een naadloze integratie met SwiftUI. Voor bestaande Core Data-toepassingen vormt de migratie een strategische stap richting moderner en beter onderhoudbare code. > **Wat dit artikel behandelt** > > Deze gids beschrijft het volledige migratieproces van Core Data naar SwiftData: compatibiliteitsbeoordeling, modelconversie, datamigratiestrategieën en coexistence-patronen voor een geleidelijke overgang. ## Beoordeling van de Migratiehaalbaarheid Voor het starten van de migratie maakt een grondige beoordeling het mogelijk potentiële obstakels te identificeren. Core Data en SwiftData delen dezelfde SQLite-persistentie-engine, waardoor de gegevens volledig compatibel zijn. ```swift // MigrationAssessment.swift // Migration assessment checklist /* FEATURES SUPPORTED BY SWIFTDATA: ✅ Simple models with basic properties ✅ One-to-one and one-to-many relationships ✅ Optional properties and default values ✅ Transformable attributes (via Codable) ✅ CloudKit synchronization (basic) ✅ Automatic lightweight migrations ✅ Class inheritance (iOS 26+) FEATURES REQUIRING ATTENTION: ⚠️ NSFetchedResultsController → @Query + manual observation ⚠️ NSCompoundPredicate → #Predicate with combined logic ⚠️ Dynamic predicates → Workarounds required UNSUPPORTED FEATURES: ❌ Advanced CloudKit Sharing ❌ Derived attributes ❌ Fetched properties */ // Example of typical Core Data model to migrate import CoreData // Existing Core Data entity class CDTask: NSManagedObject { @NSManaged var id: UUID @NSManaged var title: String @NSManaged var isCompleted: Bool @NSManaged var createdAt: Date @NSManaged var priority: Int16 @NSManaged var category: CDCategory? } class CDCategory: NSManagedObject { @NSManaged var id: UUID @NSManaged var name: String @NSManaged var color: String @NSManaged var tasks: NSSet? } ``` Compatibiliteit op datablock-niveau betekent dat gebruikers hun bestaande informatie behouden na de migratie. Er treedt geen dataverlies op wanneer het proces correct wordt uitgevoerd. ## Conversie van Core Data-modellen naar SwiftData De eerste concrete stap bestaat uit het converteren van Core Data-entiteiten naar SwiftData-klassen. Xcode biedt een automatisch hulpmiddel, maar het begrip van het handmatige proces blijft essentieel. ```swift // TaskModel.swift import SwiftData // SwiftData equivalent of CDTask @Model final class Task { // Properties with default values var id: UUID = UUID() var title: String = "" var isCompleted: Bool = false var createdAt: Date = Date() var priority: Int = 0 // Optional relationship to Category var category: Category? // Explicit initializer recommended init( id: UUID = UUID(), title: String, isCompleted: Bool = false, createdAt: Date = Date(), priority: Int = 0, category: Category? = nil ) { self.id = id self.title = title self.isCompleted = isCompleted self.createdAt = createdAt self.priority = priority self.category = category } } ``` Belangrijke verschillen met Core Data zijn het gebruik van de `@Model`-macro in plaats van `NSManagedObject`, en native Swift-types in plaats van Objective-C-types. ```swift // CategoryModel.swift import SwiftData @Model final class Category { var id: UUID = UUID() var name: String = "" var color: String = "blue" // Inverse relationship with delete rule @Relationship(deleteRule: .cascade, inverse: \Task.category) var tasks: [Task] = [] init(id: UUID = UUID(), name: String, color: String = "blue") { self.id = id self.name = name self.color = color } } ``` > **Type-mapping** > > Core Data-types converteren rechtstreeks: `Int16` wordt `Int`, `NSSet` wordt `[Model]` en `Date` blijft `Date`. `Transformable`-attributen vereisen het toepassen van `Codable`. ## Configuratie van de ModelContainer De `ModelContainer` van SwiftData vervangt de `NSPersistentContainer` van Core Data. De configuratie bepaalt waar en hoe de gegevens worden opgeslagen. ```swift // ModelContainerSetup.swift import SwiftData import SwiftUI @main struct TaskManagerApp: App { // SwiftData container configuration var sharedModelContainer: ModelContainer = { // Schema including all models let schema = Schema([ Task.self, Category.self ]) // Configuration with storage options let modelConfiguration = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, // Use the same store as Core Data url: URL.applicationSupportDirectory .appending(path: "TaskManager.sqlite") ) do { return try ModelContainer( for: schema, configurations: [modelConfiguration] ) } catch { fatalError("Could not create ModelContainer: \(error)") } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(sharedModelContainer) } } ``` Het cruciale punt zit in de URL van de store: door hetzelfde SQLite-bestand als Core Data te gebruiken, kan SwiftData de bestaande gegevens lezen. ## Coexistence-strategie tussen Core Data en SwiftData Voor complexe applicaties vormt een geleidelijke migratie via coexistence van beide frameworks de veiligste aanpak. Beide stacks kunnen toegang krijgen tot hetzelfde SQLite-bestand. ```swift // CoexistenceSetup.swift import CoreData import SwiftData // Configuration for coexistence class PersistenceController { static let shared = PersistenceController() // Shared store between Core Data and SwiftData private let storeURL: URL = { let appSupport = FileManager.default .urls(for: .applicationSupportDirectory, in: .userDomainMask) .first! return appSupport.appending(path: "TaskManager.sqlite") }() // MARK: - Core Data Stack (existing) lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "TaskManager") // Configure to use shared store let description = NSPersistentStoreDescription(url: storeURL) description.setOption( true as NSNumber, forKey: NSPersistentHistoryTrackingKey ) container.persistentStoreDescriptions = [description] container.loadPersistentStores { _, error in if let error = error as NSError? { fatalError("Core Data error: \(error)") } } return container }() // MARK: - SwiftData Stack (new) lazy var swiftDataContainer: ModelContainer = { let schema = Schema([Task.self, Category.self]) let config = ModelConfiguration( schema: schema, url: storeURL, // Disable automatic migrations in coexistence allowsSave: true ) do { return try ModelContainer(for: schema, configurations: [config]) } catch { fatalError("SwiftData error: \(error)") } }() } ``` > **Synchronisatie van wijzigingen** > > In coexistence-modus zijn wijzigingen door het ene framework niet onmiddellijk zichtbaar voor het andere. Een expliciete reload of herstart van de app kan nodig zijn. ## Migratie van Queries: Van NSFetchRequest naar @Query Het belangrijkste verschil betreft de manier waarop gegevens worden opgehaald. SwiftUI gebruikt de property wrapper `@Query` om `@FetchRequest` te vervangen. ```swift // QueryMigration.swift import SwiftUI import SwiftData // ❌ Old pattern with Core Data struct OldTaskListView: View { @FetchRequest( sortDescriptors: [ NSSortDescriptor(keyPath: \CDTask.createdAt, ascending: false) ], predicate: NSPredicate(format: "isCompleted == NO") ) private var tasks: FetchedResults var body: some View { List(tasks) { task in Text(task.title) } } } // ✅ New pattern with SwiftData struct NewTaskListView: View { // @Query with built-in sorting and filtering @Query( filter: #Predicate { !$0.isCompleted }, sort: \Task.createdAt, order: .reverse ) private var tasks: [Task] var body: some View { List(tasks) { task in TaskRowView(task: task) } } } struct TaskRowView: View { let task: Task var body: some View { HStack { // Priority indicator Circle() .fill(priorityColor) .frame(width: 8, height: 8) VStack(alignment: .leading) { Text(task.title) .font(.headline) if let category = task.category { Text(category.name) .font(.caption) .foregroundStyle(.secondary) } } Spacer() // Date badge Text(task.createdAt, style: .date) .font(.caption2) .foregroundStyle(.tertiary) } } private var priorityColor: Color { switch task.priority { case 3: return .red case 2: return .orange case 1: return .yellow default: return .gray } } } ``` ## Omgaan met Dynamische Predicates Een belangrijke uitdaging bij SwiftData betreft dynamische predicates. In tegenstelling tot Core Data, waar predicates ter plekke kunnen worden aangepast, vereist `@Query` alternatieve benaderingen. ```swift // DynamicPredicates.swift import SwiftUI import SwiftData // Solution 1: Use @Query with custom init struct FilteredTasksView: View { @Query private var tasks: [Task] // Create view with specific filter init(showCompleted: Bool, categoryId: UUID?) { // Build predicate based on parameters var predicates: [Predicate] = [] if !showCompleted { predicates.append(#Predicate { !$0.isCompleted }) } if let categoryId { predicates.append(#Predicate { task in task.category?.id == categoryId }) } // Combine predicates let combinedPredicate: Predicate? if predicates.isEmpty { combinedPredicate = nil } else if predicates.count == 1 { combinedPredicate = predicates[0] } else { // Manually combine for AND logic combinedPredicate = #Predicate { task in !task.isCompleted && task.category?.id == categoryId } } _tasks = Query( filter: combinedPredicate, sort: \Task.createdAt, order: .reverse ) } var body: some View { List(tasks) { task in TaskRowView(task: task) } } } // Solution 2: View-side filtering with all results struct SmartTaskListView: View { // Fetch all tasks @Query(sort: \Task.createdAt, order: .reverse) private var allTasks: [Task] // Filter state @State private var searchText = "" @State private var showCompleted = false @State private var selectedCategory: Category? // Computed filtering private var filteredTasks: [Task] { allTasks.filter { task in // Text filter let matchesSearch = searchText.isEmpty || task.title.localizedCaseInsensitiveContains(searchText) // Status filter let matchesStatus = showCompleted || !task.isCompleted // Category filter let matchesCategory = selectedCategory == nil || task.category?.id == selectedCategory?.id return matchesSearch && matchesStatus && matchesCategory } } var body: some View { NavigationStack { List(filteredTasks) { task in TaskRowView(task: task) } .searchable(text: $searchText) .toolbar { FilterMenu( showCompleted: $showCompleted, selectedCategory: $selectedCategory ) } } } } ``` ## Versiegebaseerde Schema-migraties Wanneer het datamodel evolueert, gebruikt SwiftData `VersionedSchema` om complexe migraties te beheren. ```swift // VersionedSchemas.swift import SwiftData // Version 1: Initial schema 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 = UUID() var title: String = "" var isCompleted: Bool = false var createdAt: Date = Date() var category: Category? init(title: String) { self.title = title } } @Model final class Category { var id: UUID = UUID() var name: String = "" @Relationship(deleteRule: .cascade, inverse: \Task.category) var tasks: [Task] = [] init(name: String) { self.name = name } } } // Version 2: Added priority and notes fields enum TaskSchemaV2: VersionedSchema { static var versionIdentifier = Schema.Version(2, 0, 0) static var models: [any PersistentModel.Type] { [Task.self, Category.self] } @Model final class Task { var id: UUID = UUID() var title: String = "" var isCompleted: Bool = false var createdAt: Date = Date() // New properties with default values var priority: Int = 0 var notes: String = "" var category: Category? init(title: String, priority: Int = 0) { self.title = title self.priority = priority } } @Model final class Category { var id: UUID = UUID() var name: String = "" // New property var color: String = "blue" @Relationship(deleteRule: .cascade, inverse: \Task.category) var tasks: [Task] = [] init(name: String, color: String = "blue") { self.name = name self.color = color } } } ``` Het migratieplan definieert de volgorde van de versies en eventueel benodigde aangepaste migraties. ```swift // MigrationPlan.swift import SwiftData enum TaskMigrationPlan: SchemaMigrationPlan { // Chronological order of schemas static var schemas: [any VersionedSchema.Type] { [TaskSchemaV1.self, TaskSchemaV2.self] } // Migration stages static var stages: [MigrationStage] { [migrateV1toV2] } // V1 → V2 migration: lightweight (properties with defaults) static let migrateV1toV2 = MigrationStage.lightweight( fromVersion: TaskSchemaV1.self, toVersion: TaskSchemaV2.self ) } // Container configuration with migration @main struct TaskManagerApp: App { var sharedModelContainer: ModelContainer = { do { return try ModelContainer( for: TaskSchemaV2.Task.self, TaskSchemaV2.Category.self, migrationPlan: TaskMigrationPlan.self ) } catch { fatalError("Migration failed: \(error)") } }() var body: some Scene { WindowGroup { ContentView() } .modelContainer(sharedModelContainer) } } ``` > **Lightweight versus aangepaste migraties** > > SwiftData verwerkt automatisch lightweight-migraties (toevoegen van eigenschappen met default-waarden, hernoemen, verwijderen). Complexe migraties die data-transformatie vereisen, gebruiken `MigrationStage.custom`. ## Vervanging van NSFetchedResultsController Voor lijsten met secties of gedetailleerde observatie van wijzigingen vervangt `@Query` in combinatie met data-extractie de `NSFetchedResultsController`. ```swift // SectionedResults.swift import SwiftUI import SwiftData struct SectionedTaskListView: View { @Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task] // Grouping by category private var tasksByCategory: [(Category?, [Task])] { Dictionary(grouping: tasks) { $0.category } .map { ($0.key, $0.value) } .sorted { first, second in // Tasks without category last guard let firstName = first.0?.name else { return false } guard let secondName = second.0?.name else { return true } return firstName < secondName } } var body: some View { List { ForEach(tasksByCategory, id: \.0?.id) { category, categoryTasks in Section(header: SectionHeader(category: category)) { ForEach(categoryTasks) { task in TaskRowView(task: task) } } } } } } struct SectionHeader: View { let category: Category? var body: some View { HStack { if let category { Circle() .fill(Color(category.color)) .frame(width: 12, height: 12) Text(category.name) } else { Text("Uncategorized") .foregroundStyle(.secondary) } } } } // Alternative: Grouping by date struct DateGroupedTasksView: View { @Query(sort: \Task.createdAt, order: .reverse) private var tasks: [Task] private var tasksByDate: [(Date, [Task])] { let calendar = Calendar.current let grouped = Dictionary(grouping: tasks) { task in calendar.startOfDay(for: task.createdAt) } return grouped .map { ($0.key, $0.value) } .sorted { $0.0 > $1.0 } } var body: some View { List { ForEach(tasksByDate, id: \.0) { date, dateTasks in Section(header: Text(date, style: .date)) { ForEach(dateTasks) { task in TaskRowView(task: task) } } } } } } ``` ## CRUD-operaties met ModelContext De `ModelContext` vervangt de `NSManagedObjectContext` voor alle aanmaak-, lees-, update- en verwijderoperaties. ```swift // CRUDOperations.swift import SwiftUI import SwiftData struct TaskManagementView: View { @Environment(\.modelContext) private var modelContext @Query private var tasks: [Task] @Query private var categories: [Category] @State private var newTaskTitle = "" @State private var selectedCategory: Category? var body: some View { NavigationStack { VStack { // Add form AddTaskForm( title: $newTaskTitle, category: $selectedCategory, categories: categories, onAdd: addTask ) // Task list List { ForEach(tasks) { task in TaskRowView(task: task) .swipeActions(edge: .trailing) { Button(role: .destructive) { deleteTask(task) } label: { Label("Delete", systemImage: "trash") } } .swipeActions(edge: .leading) { Button { toggleCompletion(task) } label: { Label( task.isCompleted ? "Todo" : "Done", systemImage: task.isCompleted ? "circle" : "checkmark" ) } .tint(task.isCompleted ? .orange : .green) } } } } .navigationTitle("Tasks") } } // CREATE private func addTask() { guard !newTaskTitle.isEmpty else { return } let task = Task( title: newTaskTitle, category: selectedCategory ) // Insert into context modelContext.insert(task) // Explicit save (optional - autosave enabled by default) do { try modelContext.save() } catch { print("Save error: \(error)") } // Reset form newTaskTitle = "" selectedCategory = nil } // UPDATE private func toggleCompletion(_ task: Task) { // Direct modification - SwiftData tracks automatically task.isCompleted.toggle() // Automatic save handles persistence } // DELETE private func deleteTask(_ task: Task) { modelContext.delete(task) } } struct AddTaskForm: View { @Binding var title: String @Binding var category: Category? let categories: [Category] let onAdd: () -> Void var body: some View { VStack(spacing: 12) { TextField("New task...", text: $title) .textFieldStyle(.roundedBorder) HStack { Picker("Category", selection: $category) { Text("None").tag(nil as Category?) ForEach(categories) { cat in Text(cat.name).tag(cat as Category?) } } .pickerStyle(.menu) Button("Add", action: onAdd) .buttonStyle(.borderedProminent) .disabled(title.isEmpty) } } .padding() } } ``` ## Unit Tests met SwiftData Een robuuste teststrategie vergemakkelijkt de validatie van de migratie. SwiftData maakt het mogelijk om in-memory containers te creëren voor tests. ```swift // SwiftDataTests.swift import XCTest import SwiftData @testable import TaskManager final class TaskModelTests: XCTestCase { var container: ModelContainer! var context: ModelContext! override func setUpWithError() throws { // In-memory container for tests let config = ModelConfiguration(isStoredInMemoryOnly: true) container = try ModelContainer( for: Task.self, Category.self, configurations: config ) context = ModelContext(container) } override func tearDownWithError() throws { container = nil context = nil } func testCreateTask() throws { // Given let task = Task(title: "Test Task") // When context.insert(task) try context.save() // Then let descriptor = FetchDescriptor() let tasks = try context.fetch(descriptor) XCTAssertEqual(tasks.count, 1) XCTAssertEqual(tasks.first?.title, "Test Task") } func testTaskCategoryRelationship() throws { // Given let category = Category(name: "Work", color: "blue") let task = Task(title: "Meeting", category: category) // When context.insert(category) context.insert(task) try context.save() // Then XCTAssertEqual(task.category?.name, "Work") XCTAssertTrue(category.tasks.contains(task)) } func testDeleteCategoryCascade() throws { // Given let category = Category(name: "Personal") let task1 = Task(title: "Task 1", category: category) let task2 = Task(title: "Task 2", category: category) context.insert(category) context.insert(task1) context.insert(task2) try context.save() // When context.delete(category) try context.save() // Then - cascade delete should remove tasks let descriptor = FetchDescriptor() let remainingTasks = try context.fetch(descriptor) XCTAssertEqual(remainingTasks.count, 0) } func testFilteredFetch() throws { // Given let task1 = Task(title: "Completed", isCompleted: true) let task2 = Task(title: "Pending", isCompleted: false) let task3 = Task(title: "Also Pending", isCompleted: false) [task1, task2, task3].forEach { context.insert($0) } try context.save() // When var descriptor = FetchDescriptor( predicate: #Predicate { !$0.isCompleted } ) let pendingTasks = try context.fetch(descriptor) // Then XCTAssertEqual(pendingTasks.count, 2) } } ``` ## Volledige Migratie-checklist Hieronder een samenvatting van de stappen voor een succesvolle migratie: ```swift // MigrationChecklist.swift /* PHASE 1: PREPARATION □ Audit Core Data features in use □ Identify features not supported by SwiftData □ Create dedicated migration branch □ Back up test data PHASE 2: MODEL CONVERSION □ Convert NSManagedObject entities to @Model □ Adapt relationships with @Relationship □ Configure appropriate delete rules □ Add required default values PHASE 3: CONFIGURATION □ Create ModelContainer with existing store URL □ Configure versioned schema if needed □ Define migration plan □ Test in coexistence mode if applicable PHASE 4: CODE MIGRATION □ Replace @FetchRequest with @Query □ Adapt predicates to #Predicate □ Migrate NSFetchedResultsController to manual grouping □ Convert CRUD operations to ModelContext PHASE 5: VALIDATION □ Run all unit tests □ Test migration with real data □ Verify performance with Instruments □ Validate CloudKit sync (if applicable) PHASE 6: DEPLOYMENT □ Document breaking changes □ Prepare rollback plan □ Deploy to TestFlight □ Monitor post-deployment crashes */ ``` ## Conclusie De migratie van Core Data naar SwiftData vertegenwoordigt een natuurlijke evolutie voor moderne iOS-applicaties. Compatibiliteit op SQLite-store-niveau garandeert het behoud van gebruikersgegevens, terwijl de native Swift-syntax de code aanzienlijk vereenvoudigt. ### Belangrijkste Punten - ✅ SwiftData en Core Data delen dezelfde SQLite-engine - ✅ Coexistence maakt een geleidelijke migratie mogelijk - ✅ `@Query` vervangt `@FetchRequest` met eenvoudigere syntax - ✅ Dynamische predicates vereisen alternatieve patronen - ✅ `VersionedSchema` beheert de schema-evolutie - ✅ In-memory tests vergemakkelijken de validatie - ✅ iOS 26 brengt ondersteuning voor klasse-overerving - ✅ Het is verstandig nieuwe projecten met SwiftData te starten, behalve bij specifieke Core Data-behoeften --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/ios/core-data-swiftdata-migration-guide