If you're an iOS developer still using Core Data, it's time to seriously consider SwiftData. Introduced at WWDC 2023, SwiftData is Apple's modern persistence framework built on top of Core Data but with a completely Swift-native API. In this guide, I'll walk you through everything you need to know to migrate from Core Data to SwiftData in 2026.
What is SwiftData?
SwiftData is Apple's declarative data persistence framework for Swift. It replaces the verbose, Objective-C-era APIs of Core Data with clean, macro-driven Swift code. Think of it as what Core Data would look like if it were designed from scratch for SwiftUI.
Key features:
- Swift-native -- no more NSManagedObject subclasses
-
Macro-based -- uses
@Modelinstead of .xcdatamodeld files -
SwiftUI integration -- works seamlessly with
@Queryproperty wrapper -
Type-safe queries --
#PredicatereplacesNSPredicate - Automatic schema migration -- handles simple migrations out of the box
SwiftData vs Core Data: Side-by-Side Comparison
| Feature | Core Data | SwiftData |
|---|---|---|
| Model Definition | .xcdatamodeld + NSManagedObject | @Model macro |
| Context | NSManagedObjectContext | ModelContext |
| Container | NSPersistentContainer | ModelContainer |
| Queries | NSFetchRequest + NSPredicate | @Query + #Predicate |
| Sorting | NSSortDescriptor | SortDescriptor |
| Relationships | Manual configuration | Automatic inference |
| SwiftUI Integration | @FetchRequest | @Query |
| Minimum Target | iOS 3+ | iOS 17+ |
| Schema Editor | Visual editor in Xcode | Pure Swift code |
| Migration | Mapping models | Automatic / VersionedSchema |
Setting Up SwiftData
With Core Data, you needed a .xcdatamodeld file, entity configurations, and NSManagedObject subclasses. With SwiftData, everything is pure Swift.
Defining a Model
import SwiftData
@Model
final class Task {
var title: String
var notes: String
var isCompleted: Bool
var dueDate: Date?
var priority: Int
var createdAt: Date
init(title: String, notes: String = "", isCompleted: Bool = false, dueDate: Date? = nil, priority: Int = 0) {
self.title = title
self.notes = notes
self.isCompleted = isCompleted
self.dueDate = dueDate
self.priority = priority
self.createdAt = Date()
}
}
The @Model macro automatically makes your class persistable. No more NSManagedObject subclassing or XML schema files.
Setting Up ModelContainer
import SwiftUI
import SwiftData
@main
struct MyTaskApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Task.self, Category.self])
}
}
Compare this to the old Core Data stack setup that required 20+ lines of boilerplate with NSPersistentContainer, loading persistent stores, and error handling. SwiftData does it in one line.
Accessing ModelContext
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
var body: some View {
// modelContext is ready to use
Text("Tasks")
}
}
CRUD Operations with SwiftData
Create
func addTask(title: String, priority: Int) {
let task = Task(title: title, priority: priority)
modelContext.insert(task)
// SwiftData auto-saves. No need to call save() manually.
}
In Core Data, you had to create an object via the context, set properties individually, and explicitly call try context.save(). SwiftData handles saving automatically.
Read with @Query
struct TaskListView: View {
@Query(sort: \Task.createdAt, order: .reverse)
private var tasks: [Task]
var body: some View {
List(tasks) { task in
TaskRow(task: task)
}
}
}
Update
func toggleCompletion(for task: Task) {
task.isCompleted.toggle()
// That's it. SwiftData tracks changes automatically.
}
No context.save() needed -- SwiftData observes property changes and persists them.
Delete
func deleteTask(_ task: Task) {
modelContext.delete(task)
}
// Delete multiple tasks
func deleteCompleted() {
try? modelContext.delete(model: Task.self, where: #Predicate<Task> { task in
task.isCompleted == true
})
}
Relationships
SwiftData makes relationships straightforward. Just use standard Swift properties.
One-to-Many
@Model
final class Category {
var name: String
var color: String
@Relationship(deleteRule: .cascade)
var tasks: [Task] = []
init(name: String, color: String) {
self.name = name
self.color = color
}
}
@Model
final class Task {
var title: String
var isCompleted: Bool
var category: Category?
init(title: String, isCompleted: Bool = false, category: Category? = nil) {
self.title = title
self.isCompleted = isCompleted
self.category = category
}
}
Many-to-Many
@Model
final class Task {
var title: String
@Relationship
var tags: [Tag] = []
init(title: String) {
self.title = title
}
}
@Model
final class Tag {
var name: String
@Relationship(inverse: \Task.tags)
var tasks: [Task] = []
init(name: String) {
self.name = name
}
}
SwiftData infers the inverse relationship automatically, but you can be explicit with the inverse parameter.
Querying with #Predicate
The #Predicate macro brings type-safe, compile-time-checked queries. No more string-based NSPredicate that crashes at runtime.
// Filter incomplete high-priority tasks
@Query(filter: #Predicate<Task> { task in
!task.isCompleted && task.priority >= 3
})
private var urgentTasks: [Task]
// Search tasks by title
func searchTasks(keyword: String) -> [Task] {
let predicate = #Predicate<Task> { task in
task.title.localizedStandardContains(keyword)
}
let descriptor = FetchDescriptor<Task>(predicate: predicate)
return (try? modelContext.fetch(descriptor)) ?? []
}
// Complex compound predicates
let predicate = #Predicate<Task> { task in
task.isCompleted == false &&
task.priority > 2 &&
(task.dueDate ?? Date.distantFuture) < Date.now
}
Compare this with Core Data's NSPredicate:
// Old Core Data way -- string-based, error-prone
let predicate = NSPredicate(format: "isCompleted == %@ AND priority > %d", NSNumber(value: false), 2)
Sorting with SortDescriptor
// Single sort
@Query(sort: \Task.dueDate)
private var tasksByDate: [Task]
// Multiple sorts
@Query(sort: [
SortDescriptor(\Task.priority, order: .reverse),
SortDescriptor(\Task.createdAt, order: .reverse)
])
private var sortedTasks: [Task]
// Dynamic sorting with FetchDescriptor
func fetchTasks(sortBy keyPath: KeyPath<Task, some Comparable>) -> [Task] {
var descriptor = FetchDescriptor<Task>()
descriptor.sortBy = [SortDescriptor(keyPath)]
descriptor.fetchLimit = 50
return (try? modelContext.fetch(descriptor)) ?? []
}
Migration from Core Data: Step-by-Step
Step 1: Create SwiftData Models
Convert your NSManagedObject subclasses to @Model classes. Map each entity to a new Swift class:
// Before: Core Data NSManagedObject
class CDTask: NSManagedObject {
@NSManaged var title: String?
@NSManaged var isCompleted: Bool
@NSManaged var createdAt: Date?
}
// After: SwiftData @Model
@Model
final class Task {
var title: String
var isCompleted: Bool
var createdAt: Date
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
self.createdAt = Date()
}
}
Step 2: Set Up ModelContainer with Migration
@main
struct MyApp: App {
let container: ModelContainer
init() {
do {
let schema = Schema([Task.self, Category.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
container = try ModelContainer(for: schema, configurations: [config])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
Step 3: Versioned Schema for Complex Migrations
enum TaskSchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Task.self] }
@Model
final class Task {
var title: String
var isCompleted: Bool
init(title: String) {
self.title = title
self.isCompleted = false
}
}
}
enum TaskSchemaV2: VersionedSchema {
static var versionIdentifier = Schema.Version(2, 0, 0)
static var models: [any PersistentModel.Type] { [Task.self] }
@Model
final class Task {
var title: String
var isCompleted: Bool
var priority: Int // New field
init(title: String, priority: Int = 0) {
self.title = title
self.isCompleted = false
self.priority = priority
}
}
}
enum TaskMigrationPlan: SchemaMigrationPlan {
static var schemas: [any VersionedSchema.Type] {
[TaskSchemaV1.self, TaskSchemaV2.self]
}
static var stages: [MigrationStage] {
[migrateV1toV2]
}
static let migrateV1toV2 = MigrationStage.lightweight(
fromVersion: TaskSchemaV1.self,
toVersion: TaskSchemaV2.self
)
}
Step 4: Replace @FetchRequest with @Query
// Before
@FetchRequest(sortDescriptors: [NSSortDescriptor(keyPath: \CDTask.createdAt, ascending: false)])
var tasks: FetchedResults<CDTask>
// After
@Query(sort: \Task.createdAt, order: .reverse)
var tasks: [Task]
Step 5: Migrate Existing Data
If you need to transfer existing Core Data records to SwiftData, write a one-time migration:
func migrateFromCoreData(coreDataContext: NSManagedObjectContext, modelContext: ModelContext) {
let fetchRequest: NSFetchRequest<CDTask> = CDTask.fetchRequest()
guard let coreDataTasks = try? coreDataContext.fetch(fetchRequest) else { return }
for cdTask in coreDataTasks {
let newTask = Task(
title: cdTask.title ?? "Untitled",
isCompleted: cdTask.isCompleted
)
modelContext.insert(newTask)
}
}
When to Use Which: Decision Framework
Use SwiftData when:
- Starting a new project targeting iOS 17+
- Building a SwiftUI-first app
- You want minimal boilerplate
- Your data model is straightforward
- You need quick prototyping
Stick with Core Data when:
- You need to support iOS 16 or earlier
- You have a large existing Core Data codebase
- You need advanced features like CloudKit public databases
- Your app uses UIKit primarily
- You need fine-grained performance control with batch operations
Hybrid approach:
- Use SwiftData for new features while keeping Core Data for legacy code
- Both can coexist in the same project since SwiftData is built on Core Data's foundation
Conclusion
SwiftData is the future of data persistence on Apple platforms. It dramatically reduces boilerplate, provides type safety, and integrates beautifully with SwiftUI. If you're starting a new project in 2026, SwiftData should be your default choice.
For existing apps, plan a gradual migration -- start with new features and migrate existing code module by module. The investment pays off in cleaner, more maintainable code.
My SwiftUI iOS 18 App Templates use SwiftData for all persistence. Check them out: https://pease163.github.io/digital-products/
What's your experience with SwiftData? Have you migrated from Core Data? Share your thoughts in the comments below!
Top comments (0)