What is SwiftData and how does it compare to Core Data?
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")..cascadedeletes 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:
#Indexand#Uniquemacros, history tracking, custom data stores. - iOS 26: model inheritance (a
@Modelsubclass of another@Model).
SwiftData vs Core Data
| SwiftData | Core Data | |
|---|---|---|
| Minimum iOS | 17 | 3 |
| Model definition | Swift code (@Model) | .xcdatamodeld editor |
| Queries | #Predicate, type-safe | NSPredicate strings |
| SwiftUI | @Query, @Observable built in | @FetchRequest, ObservableObject |
| Maturity | Newer, fewer advanced features | Very mature |
| Advanced features | Fewer | Batch 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
.uniqueis not allowed. - How do you preview with fake data? Create a container with
ModelConfiguration(isStoredInMemoryOnly: true). - Why is my
@Querynot filtering by a variable?@Queryis set at init. Build it in the view'sinitwith the value passed in.