# Migracja z Core Data do SwiftData: Przewodnik krok po kroku 2026 > Kompletny przewodnik migracji aplikacji iOS z Core Data do SwiftData z praktycznymi przykładami, strategiami koegzystencji i najlepszymi praktykami. - Published: 2026-02-27 - Updated: 2026-04-29 - Author: SharpSkill - Tags: swiftdata, core-data, ios, migration, swift - Reading time: 16 min --- SwiftData reprezentuje przyszłość trwałości danych na platformach Apple. Zaprezentowany na WWDC 2023 framework oferuje natywną składnię Swift oraz płynną integrację z SwiftUI. Dla istniejących aplikacji opartych na Core Data migracja stanowi strategiczny krok w stronę nowocześniejszego i łatwiejszego w utrzymaniu kodu. > **Czego dotyczy ten artykuł** > > Ten przewodnik szczegółowo opisuje pełny proces migracji z Core Data do SwiftData: ocenę kompatybilności, konwersję modeli, strategie migracji danych oraz wzorce koegzystencji dla stopniowego przejścia. ## Ocena Wykonalności Migracji Przed rozpoczęciem migracji rzetelna ocena pozwala zidentyfikować potencjalne przeszkody. Core Data i SwiftData współdzielą ten sam silnik trwałości SQLite, dzięki czemu dane są w pełni kompatybilne. ```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? } ``` Kompatybilność na poziomie danych oznacza, że użytkownicy zachowują istniejące informacje po migracji. Przy poprawnie wykonanym procesie nie dochodzi do utraty danych. ## Konwersja Modeli Core Data do SwiftData Pierwszym konkretnym krokiem jest konwersja encji Core Data na klasy SwiftData. Xcode oferuje narzędzie automatyczne, ale zrozumienie procesu manualnego pozostaje kluczowe. ```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 } } ``` Główne różnice w stosunku do Core Data obejmują użycie makra `@Model` zamiast `NSManagedObject` oraz natywne typy Swift zamiast typów Objective-C. ```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 } } ``` > **Mapowanie typów** > > Typy z Core Data konwertują się bezpośrednio: `Int16` zmienia się w `Int`, `NSSet` w `[Model]`, a `Date` pozostaje `Date`. Atrybuty `Transformable` wymagają przyjęcia `Codable`. ## Konfiguracja ModelContainer `ModelContainer` z SwiftData zastępuje `NSPersistentContainer` z Core Data. Konfiguracja określa, gdzie i w jaki sposób przechowywane są dane. ```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) } } ``` Kluczowy punkt to URL store'a: użycie tego samego pliku SQLite, co Core Data, pozwala SwiftData odczytywać istniejące dane. ## Strategia Koegzystencji Core Data i SwiftData W przypadku złożonych aplikacji stopniowa migracja poprzez koegzystencję obu frameworków stanowi najbezpieczniejsze podejście. Oba stosy mogą uzyskiwać dostęp do tego samego pliku SQLite. ```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)") } }() } ``` > **Synchronizacja zmian** > > W trybie koegzystencji zmiany wprowadzone przez jeden framework nie są natychmiast widoczne dla drugiego. Może być wymagane jawne ponowne wczytanie lub restart aplikacji. ## Migracja Zapytań: Z NSFetchRequest do @Query Najistotniejsza różnica dotyczy sposobu pobierania danych. SwiftUI wykorzystuje property wrapper `@Query` w zastępstwie `@FetchRequest`. ```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 } } } ``` ## Obsługa Predykatów Dynamicznych Istotnym wyzwaniem w SwiftData są predykaty dynamiczne. W przeciwieństwie do Core Data, gdzie predykaty można modyfikować w locie, `@Query` wymaga alternatywnych podejść. ```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 ) } } } } ``` ## Wersjonowane Migracje Schematu Gdy model danych ewoluuje, SwiftData wykorzystuje `VersionedSchema` do zarządzania złożonymi migracjami. ```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 } } } ``` Plan migracji definiuje kolejność wersji oraz ewentualne wymagane migracje niestandardowe. ```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) } } ``` > **Migracje lightweight vs niestandardowe** > > SwiftData automatycznie obsługuje migracje lightweight (dodawanie właściwości z wartościami domyślnymi, zmianę nazwy, usuwanie). Złożone migracje wymagające transformacji danych korzystają z `MigrationStage.custom`. ## Zastąpienie NSFetchedResultsController W przypadku list z sekcjami lub szczegółowej obserwacji zmian `@Query` w połączeniu z ekstrakcją danych zastępuje `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) } } } } } } ``` ## Operacje CRUD z ModelContext `ModelContext` zastępuje `NSManagedObjectContext` we wszystkich operacjach tworzenia, odczytu, aktualizacji i usuwania. ```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() } } ``` ## Testy Jednostkowe z SwiftData Solidna strategia testów ułatwia walidację migracji. SwiftData umożliwia tworzenie kontenerów w pamięci na potrzeby testów. ```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) } } ``` ## Kompletna Lista Kontrolna Migracji Poniżej znajduje się podsumowanie etapów udanej migracji: ```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 */ ``` ## Podsumowanie Migracja z Core Data do SwiftData stanowi naturalną ewolucję dla nowoczesnych aplikacji iOS. Kompatybilność na poziomie store'a SQLite gwarantuje zachowanie danych użytkownika, a natywna składnia Swift znacząco upraszcza kod. ### Najważniejsze Wnioski - ✅ SwiftData i Core Data wykorzystują ten sam silnik SQLite - ✅ Koegzystencja umożliwia stopniową migrację - ✅ `@Query` zastępuje `@FetchRequest` prostszą składnią - ✅ Predykaty dynamiczne wymagają alternatywnych wzorców - ✅ `VersionedSchema` zarządza ewolucją schematu - ✅ Testy w pamięci ułatwiają walidację - ✅ iOS 26 wprowadza wsparcie dla dziedziczenia klas - ✅ Warto rozpoczynać nowe projekty od SwiftData, chyba że istnieją specyficzne wymogi Core Data --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pl/blog/ios/core-data-swiftdata-migration-guide