iosNudge

What is SwiftData and how does it compare to Core Data?

MediumAsked oftenPersistenceSwiftUI 2 min read

TL;DR

SwiftData (iOS 17+) lets you persist plain Swift classes. Mark them with @Model. No model editor file, no NSManagedObject subclasses. Main parts: @Model, ModelContainer (the database), ModelContext (where you insert, delete and save) and @Query (live results in SwiftUI). It uses Core Data's storage under the hood, and it is @Observable, so views update automatically.

Define a model

import SwiftData

@Model
final class Trip {
    @Attribute(.unique) var id: UUID
    var name: String
    var startDate: Date
    var isFavorite = false
    @Relationship(deleteRule: .cascade, inverse: \Stop.trip)
    var stops: [Stop] = []

    init(name: String, startDate: Date) {
        self.id = UUID()
        self.name = name
        self.startDate = startDate
    }
}

@Model
final class Stop {
    var title: String
    var trip: Trip?
    init(title: String) { self.title = title }
}
  • @Attribute(.unique) makes inserts with the same value update the existing row (an "upsert").
  • .cascade deletes stops when their trip is deleted.

Set up the container

@main
struct TripsApp: App {
    var body: some Scene {
        WindowGroup { TripList() }
            .modelContainer(for: Trip.self)
    }
}

Read with @Query

struct TripList: View {
    @Query(filter: #Predicate<Trip> { $0.isFavorite },
           sort: \Trip.startDate)
    private var favorites: [Trip]

    @Environment(\.modelContext) private var context

    var body: some View {
        List(favorites) { trip in Text(trip.name) }
        Button("Add") {
            context.insert(Trip(name: "Lisbon", startDate: .now.addingTimeInterval(86_400)))
        }
    }
}

#Predicate is type-checked at compile time. A typo in a property name is a build error, not a crash. The main context autosaves. Call try context.save() when you need it saved right now.

Fetch outside SwiftUI

var descriptor = FetchDescriptor<Trip>(
    predicate: #Predicate { $0.name.contains("Paris") },
    sortBy: [SortDescriptor(\.startDate)]
)
descriptor.fetchLimit = 20
let trips = try context.fetch(descriptor)

Background work

ModelContext and model objects are not Sendable. Use @ModelActor:

@ModelActor
actor TripImporter {
    func importTrips(_ dtos: [TripDTO]) throws {
        for dto in dtos {
            modelContext.insert(Trip(name: dto.name, startDate: dto.start))
        }
        try modelContext.save()
    }
}

let importer = TripImporter(modelContainer: container)
try await importer.importTrips(dtos)

Pass PersistentIdentifier values between actors, not model objects.

Migrations

Describe each version with VersionedSchema and the steps with a SchemaMigrationPlan. Simple changes (add a property with a default) are lightweight and automatic.

Newer features

  • iOS 18: #Index and #Unique macros, history tracking, custom data stores.
  • iOS 26: model inheritance (a @Model subclass of another @Model).

SwiftData vs Core Data

SwiftDataCore Data
Minimum iOS173
Model definitionSwift code (@Model).xcdatamodeld editor
Queries#Predicate, type-safeNSPredicate strings
SwiftUI@Query, @Observable built in@FetchRequest, ObservableObject
MaturityNewer, fewer advanced featuresVery mature
Advanced featuresFewerBatch requests, fetched properties, derived attributes

A fair interview answer: "For a new SwiftUI app on iOS 17+, I'd start with SwiftData. For an existing Core Data app, or if I need advanced features, I'd keep Core Data."

Common follow-up questions

  • Can SwiftData and Core Data share a store? Yes. They can read the same store if the models match.
  • Does it sync with iCloud? Yes, through CloudKit. Properties need defaults or must be optional, and .unique is not allowed.
  • How do you preview with fake data? Create a container with ModelConfiguration(isStoredInMemoryOnly: true).
  • Why is my @Query not filtering by a variable? @Query is set at init. Build it in the view's init with the value passed in.