Core Data Skill
Overview
Core Data persistence patterns for macOS apps.
Stack Setup
swift
1actor PersistenceController {
2 static let shared = PersistenceController()
3
4 let container: NSPersistentContainer
5
6 init(inMemory: Bool = false) {
7 container = NSPersistentContainer(name: "AppModel")
8
9 if inMemory {
10 container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
11 }
12
13 container.loadPersistentStores { _, error in
14 if let error {
15 fatalError("Core Data failed: \(error)")
16 }
17 }
18
19 container.viewContext.automaticallyMergesChangesFromParent = true
20 container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
21 }
22
23 var viewContext: NSManagedObjectContext {
24 container.viewContext
25 }
26
27 func newBackgroundContext() -> NSManagedObjectContext {
28 container.newBackgroundContext()
29 }
30}
Entity Pattern
swift
1@objc(Item)
2public class Item: NSManagedObject {
3 @NSManaged public var id: UUID
4 @NSManaged public var title: String
5 @NSManaged public var createdAt: Date
6 @NSManaged public var updatedAt: Date
7}
8
9extension Item {
10 @nonobjc public class func fetchRequest() -> NSFetchRequest<Item> {
11 NSFetchRequest<Item>(entityName: "Item")
12 }
13
14 static func create(in context: NSManagedObjectContext, title: String) -> Item {
15 let item = Item(context: context)
16 item.id = UUID()
17 item.title = title
18 item.createdAt = Date()
19 item.updatedAt = Date()
20 return item
21 }
22}
Fetch Requests
swift
1// Basic fetch
2let request = Item.fetchRequest()
3request.predicate = NSPredicate(format: "title CONTAINS[cd] %@", searchText)
4request.sortDescriptors = [NSSortDescriptor(keyPath: \Item.createdAt, ascending: false)]
5request.fetchLimit = 50
6request.fetchBatchSize = 20
7
8let items = try context.fetch(request)
SwiftUI Integration
swift
1struct ItemListView: View {
2 @FetchRequest(
3 sortDescriptors: [SortDescriptor(\.createdAt, order: .reverse)],
4 animation: .default
5 )
6 private var items: FetchedResults<Item>
7
8 var body: some View {
9 List(items) { item in
10 Text(item.title)
11 }
12 }
13}
Background Operations
swift
1func importData(_ data: [ImportItem]) async throws {
2 let context = PersistenceController.shared.newBackgroundContext()
3
4 try await context.perform {
5 for item in data {
6 let entity = Item(context: context)
7 entity.id = UUID()
8 entity.title = item.title
9 }
10 try context.save()
11 }
12}
Migrations
- Create new model version in Xcode
- Set as current version
- Enable automatic migration:
swift
1description.shouldMigrateStoreAutomatically = true
2description.shouldInferMappingModelAutomatically = true
Testing
swift
1final class CoreDataTests: XCTestCase {
2 var controller: PersistenceController!
3
4 override func setUp() {
5 controller = PersistenceController(inMemory: true)
6 }
7}
CloudKit Container (DA-4)
For iCloud sync, use NSPersistentCloudKitContainer instead of NSPersistentContainer:
swift
1let container = NSPersistentCloudKitContainer(name: "AppModel")
2// CloudKit sync is automatic after setup
3// Conflict resolution uses NSMergeByPropertyObjectTrumpMergePolicy (local wins)
CloudKit schema migration: CloudKit schemas are additive only — you can add fields/entities but never remove or rename them. Plan schema carefully.
Derived Attributes (DA-5)
Use derived attributes for denormalized counts/aggregates to avoid expensive fetch requests:
In the Xcode model editor: select attribute > Data Model Inspector > Derived > set derivation expression.
// Count of children: "children.@count"
// Latest date: "children.@max.createdAt"
Derived attributes are computed by Core Data automatically on save. They avoid N+1 query problems.
Abstract Entity Patterns (DA-6)
Use abstract entities for shared attributes across entity types:
AbstractBaseEntity (abstract)
├── id: UUID
├── createdAt: Date
├── updatedAt: Date
│
├── TaskEntity (concrete)
│ └── title: String
│
└── NoteEntity (concrete)
└── body: String
When to use: Multiple entities share 3+ identical attributes. Avoid when: Only id/createdAt/updatedAt are shared (just add them to each entity directly — the inheritance complexity isn't worth it for 3 fields).
Core Data Debugging (DA-7)
Launch arguments for diagnostics:
| Argument | What It Shows |
|---|
-com.apple.CoreData.SQLDebug 1 | SQL queries executed |
-com.apple.CoreData.SQLDebug 3 | SQL + bind variables |
-com.apple.CoreData.MigrationDebug 1 | Migration steps |
-com.apple.CoreData.ConcurrencyDebug 1 | Thread violations |
-com.apple.CoreData.CloudKitDebug 1 | CloudKit sync activity |
Add in Xcode: Edit Scheme > Run > Arguments > Arguments Passed On Launch.
Instruments Core Data template: Shows fetch counts, fault counts, save durations. Use when debugging performance. High fault count = objects being accessed that weren't prefetched.
Data Integrity Constraints (DA-8)
Unique Constraints
Set in Xcode model editor: select entity > Data Model Inspector > Constraints. Prevents duplicate entries on the constrained fields.
// Entity: Tag
// Unique constraints: name
// → Two Tags with the same name will merge instead of creating duplicates
Validation Rules
Add in model editor per attribute: Min Value, Max Value, Regex for strings.
swift
1// Programmatic validation (for complex rules)
2override func validateForInsert() throws {
3 try super.validateForInsert()
4 guard title.count >= 1 else {
5 throw ValidationError.titleRequired
6 }
7}
Fetch Request Validation
Always validate predicates against the model at development time:
swift
1// Use typed key paths instead of string-based predicates where possible
2request.predicate = NSPredicate(format: "%K == %@", #keyPath(Item.status), "active")