Swift Core Data Skill
Data persistence framework knowledge for Core Data and SwiftData in Apple platforms.
Prerequisites
- Xcode 15+ installed
- Understanding of object graphs
- Basic SQL concepts helpful
Parameters
parameters:
framework:
type: string
enum: [core_data, swift_data]
default: swift_data
description: Persistence framework
cloudkit_sync:
type: boolean
default: false
lightweight_migration:
type: boolean
default: true
store_type:
type: string
enum: [sqlite, in_memory, binary]
default: sqlite
Topics Covered
Core Data vs SwiftData
| Feature |
Core Data |
SwiftData |
| Min iOS |
3.0+ |
17.0+ |
| Definition |
.xcdatamodeld |
@Model macro |
| Threading |
Manual (contexts) |
Actor-based |
| Fetch |
NSFetchRequest |
#Predicate |
| Learning Curve |
Steep |
Gentle |
Core Data Stack
| Component |
Purpose |
| NSPersistentContainer |
Encapsulates stack |
| NSManagedObjectContext |
Working area for objects |
| NSManagedObjectModel |
Schema definition |
| NSPersistentStoreCoordinator |
Store management |
SwiftData Components
| Component |
Purpose |
| ModelContainer |
Schema + store |
| ModelContext |
Working area |
| @Model |
Entity macro |
| @Query |
Fetch in SwiftUI |
Code Examples
SwiftData (iOS 17+)
import SwiftData
// MARK: - Model Definition
@Model
final class Task {
var title: String
var notes: String
var dueDate: Date?
var isCompleted: Bool
var priority: Priority
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Subtask.parentTask)
var subtasks: [Subtask] = []
@Relationship(inverse: \Tag.tasks)
var tags: [Tag] = []
init(title: String, notes: String = "", dueDate: Date? = nil, priority: Priority = .medium) {
self.title = title
self.notes = notes
self.dueDate = dueDate
self.isCompleted = false
self.priority = priority
self.createdAt = Date()
}
}
@Model
final class Subtask {
var title: String
var isCompleted: Bool
var parentTask: Task?
init(title: String, parentTask: Task? = nil) {
self.title = title
self.isCompleted = false
self.parentTask = parentTask
}
}
@Model
final class Tag {
@Attribute(.unique) var name: String
var color: String
var tasks: [Task] = []
init(name: String, color: String = "#007AFF") {
self.name = name
self.color = color
}
}
enum Priority: String, Codable, CaseIterable {
case low, medium, high, urgent
}
// MARK: - Container Setup
@main
struct TaskApp: App {
let container: ModelContainer
init() {
let schema = Schema([Task.self, Subtask.self, Tag.self])
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
cloudKitDatabase: .none
)
do {
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
// MARK: - SwiftUI Integration
struct TaskListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Task.dueDate) private var tasks: [Task]
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.onDelete(perform: deleteTasks)
}
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
context.delete(tasks[index])
}
}
}
// Query with predicate
struct IncompleteTasks: View {
@Query(filter: #Predicate<Task> { !$0.isCompleted },
sort: [SortDescriptor(\Task.priority, order: .reverse)])
private var tasks: [Task]
var body: some View {
List(tasks) { task in
Text(task.title)
}
}
}
Core Data (Traditional)
import CoreData
// MARK: - Core Data Stack
final class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Core Data error: \(error), \(error.userInfo)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
print("Core Data save error: \(nsError), \(nsError.userInfo)")
}
}
}
}
// MARK: - Fetch Request Helpers
extension NSManagedObjectContext {
func fetch<T: NSManagedObject>(_ type: T.Type, predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil) throws -> [T] {
let request = T.fetchRequest()
request.predicate = predicate
request.sortDescriptors = sortDescriptors
return try fetch(request) as? [T] ?? []
}
}
// MARK: - Background Operations
extension CoreDataStack {
func performBackgroundTask<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async throws -> T {
try await withCheckedThrowingContinuation { continuation in
persistentContainer.performBackgroundTask { context in
do {
let result = try block(context)
if context.hasChanges {
try context.save()
}
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
}
}
func batchInsert<T: Encodable>(entities: [T], entityName: String) async throws {
let context = newBackgroundContext()
try await context.perform {
let insertRequest = NSBatchInsertRequest(entityName: entityName, objects: entities.map { entity -> [String: Any] in
let data = try! JSONEncoder().encode(entity)
return try! JSONSerialization.jsonObject(with: data) as! [String: Any]
})
insertRequest.resultType = .count
let result = try context.execute(insertRequest) as! NSBatchInsertResult
print("Inserted \(result.result ?? 0) records")
// Merge changes to view context
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSInsertedObjectsKey: []],
into: [self.viewContext]
)
}
}
}
Migration Handling
// Lightweight Migration (automatic)
let container = NSPersistentContainer(name: "DataModel")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description?.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
// Custom Migration Manager
final class MigrationManager {
func requiresMigration(at storeURL: URL, for model: NSManagedObjectModel) -> Bool {
guard let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL) else {
return false
}
return !model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata)
}
func migrateStore(at storeURL: URL, to destinationModel: NSManagedObjectModel) throws {
// Implementation for custom migration
}
}
Troubleshooting
Common Issues
| Issue |
Cause |
Solution |
| "The model configuration is invalid" |
Schema mismatch |
Delete app, check @Model |
| Context save crash |
Wrong thread |
Use perform/performAndWait |
| Relationship fault |
Object deleted |
Check for nil before access |
| Slow fetch |
Missing index |
Add index to frequently queried attributes |
| Migration fails |
Non-trivial change |
Write custom mapping model |
Debug Tips
# Enable Core Data debug logging
# Add to scheme arguments:
-com.apple.CoreData.SQLDebug 1
-com.apple.CoreData.Logging.stderr 1
# SwiftData logging
-com.apple.SwiftData.SQLDebug 1
// Print fetch request SQL
let request: NSFetchRequest<Task> = Task.fetchRequest()
print(request.description)
// Check context state
print("Inserted: \(context.insertedObjects.count)")
print("Updated: \(context.updatedObjects.count)")
print("Deleted: \(context.deletedObjects.count)")
Validation Rules
validation:
- rule: background_for_heavy_operations
severity: error
check: Use background context for batch operations
- rule: main_thread_for_ui
severity: error
check: Only access viewContext on main thread
- rule: index_frequently_queried
severity: warning
check: Add indexes to attributes used in predicates
Usage
Skill("swift-core-data")
Related Skills
swift-networking - Syncing remote data
swift-swiftui - @Query integration
swift-testing - In-memory stores for tests