Loading...
Loading...
Build, review, or improve Core Data persistence in apps that have not adopted SwiftData. Use when working with NSManagedObject subclasses, NSFetchedResultsController for list-driven UI, NSBatchInsertRequest / NSBatchDeleteRequest / NSBatchUpdateRequest for bulk operations, NSPersistentHistoryChangeRequest for persistent history tracking and multi-target sync, NSStagedMigrationManager for staged schema migrations (iOS 17+), NSCompositeAttributeDescription for composite attributes (iOS 17+), or when integrating Core Data threading with Swift Concurrency. For Core Data + SwiftData coexistence or migration, see the swiftdata skill instead.
npx skill4agent add dpearson2699/swift-ios-skills core-dataNSPersistentContainerimport CoreData
final class CoreDataStack: @unchecked Sendable {
static let shared = CoreDataStack()
let container: NSPersistentContainer
private init() {
container = NSPersistentContainer(name: "MyAppModel")
container.loadPersistentStores { _, error in
if let error { fatalError("Core Data store failed: \(error)") }
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
var viewContext: NSManagedObjectContext { container.viewContext }
func newBackgroundContext() -> NSManagedObjectContext {
container.newBackgroundContext()
}
}NSPersistentCloudKitContainerviewContextperform(_:)performAndWait(_:)NSManagedObjectNSManagedObjectIDautomaticallyMergesChangesFromParent = trueviewContext// Writing on a background context
func updateTrip(id: NSManagedObjectID, newName: String) async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
guard let trip = try context.existingObject(with: id) as? CDTrip else {
throw PersistenceError.notFound
}
trip.name = newName
try context.save()
}
}NSManagedObjectContext.perform(_:)async throwsNSManagedObjectSendablefunc importItems(_ records: [ItemRecord]) async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
for record in records {
let item = CDItem(context: context)
item.id = record.id
item.title = record.title
}
try context.save()
}
// After save completes, viewContext auto-merges if configured
}@unchecked SendableobjectIDSendablelet objectID = trip.objectID // Sendable
Task.detached {
let bgContext = CoreDataStack.shared.newBackgroundContext()
try await bgContext.perform {
let trip = try bgContext.existingObject(with: objectID) as! CDTrip
trip.isFavorite = true
try bgContext.save()
}
}UITableViewUICollectionViewimport CoreData
import UIKit
class TripsViewController: UITableViewController, NSFetchedResultsControllerDelegate {
private lazy var fetchedResultsController: NSFetchedResultsController<CDTrip> = {
let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
request.sortDescriptors = [
NSSortDescriptor(keyPath: \CDTrip.startDate, ascending: false)
]
request.fetchBatchSize = 20
let controller = NSFetchedResultsController(
fetchRequest: request,
managedObjectContext: CoreDataStack.shared.viewContext,
sectionNameKeyPath: nil,
cacheName: "TripsCache"
)
controller.delegate = self
return controller
}()
override func viewDidLoad() {
super.viewDidLoad()
try? fetchedResultsController.performFetch()
}
// MARK: - UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
fetchedResultsController.sections?.count ?? 0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
fetchedResultsController.sections?[section].numberOfObjects ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TripCell", for: indexPath)
let trip = fetchedResultsController.object(at: indexPath)
cell.textLabel?.text = trip.name
return cell
}
// MARK: - NSFetchedResultsControllerDelegate (diffable)
func controller(
_ controller: NSFetchedResultsController<any NSFetchRequestResult>,
didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference
) {
let snapshot = snapshot as NSDiffableDataSourceSnapshot<String, NSManagedObjectID>
dataSource.apply(snapshot, animatingDifferences: true)
}
}deleteCache(withName:)cacheNamenildidChangeContentWith:reset()performFetch()func batchImport(_ records: [[String: Any]]) async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
let request = NSBatchInsertRequest(
entity: CDTrip.entity(),
objects: records
)
request.resultType = .objectIDs
let result = try context.execute(request) as? NSBatchInsertResult
if let ids = result?.result as? [NSManagedObjectID] {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSInsertedObjectsKey: ids],
into: [CoreDataStack.shared.viewContext]
)
}
}
}func deleteOldTrips(before cutoff: Date) async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = CDTrip.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "endDate < %@", cutoff as NSDate)
let request = NSBatchDeleteRequest(fetchRequest: fetchRequest)
request.resultType = .resultTypeObjectIDs
let result = try context.execute(request) as? NSBatchDeleteResult
if let ids = result?.result as? [NSManagedObjectID] {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSDeletedObjectsKey: ids],
into: [CoreDataStack.shared.viewContext]
)
}
}
}func markAllTripsAsNotFavorite() async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
let request = NSBatchUpdateRequest(entity: CDTrip.entity())
request.propertiesToUpdate = ["isFavorite": false]
request.resultType = .updatedObjectIDsResultType
let result = try context.execute(request) as? NSBatchUpdateResult
if let ids = result?.result as? [NSManagedObjectID] {
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSUpdatedObjectsKey: ids],
into: [CoreDataStack.shared.viewContext]
)
}
}
}let description = NSPersistentStoreDescription()
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.persistentStoreDescriptions = [description]// 1. Observe remote change notifications
NotificationCenter.default.addObserver(
self, selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange, object: container.persistentStoreCoordinator
)
// 2. Fetch history since last token
@objc func storeRemoteChange(_ notification: Notification) {
let context = container.newBackgroundContext()
context.perform {
let request = NSPersistentHistoryChangeRequest.fetchHistory(after: self.lastToken)
if let result = try? context.execute(request) as? NSPersistentHistoryResult,
let transactions = result.result as? [NSPersistentHistoryTransaction] {
// 3. Merge into viewContext
for transaction in transactions {
self.container.viewContext.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
self.lastToken = transaction.token
}
}
// 4. Purge old history
let purgeRequest = NSPersistentHistoryChangeRequest.deleteHistory(before: self.lastToken)
try? context.execute(purgeRequest)
}
}lastTokenUserDefaultsNSStagedMigrationManagerimport CoreData
// Define migration stages
// Use version checksums from the compiled model versions, not model names.
let checksumV1 = "<ModelV1 version checksum>"
let checksumV2 = "<ModelV2 version checksum>"
let stage1to2 = NSLightweightMigrationStage([checksumV1, checksumV2])
stage1to2.label = "Add isFavorite property"
let stage2to3 = NSCustomMigrationStage(
migratingFrom: NSManagedObjectModelReference(
named: "ModelV2", in: Bundle.main),
to: NSManagedObjectModelReference(
named: "ModelV3", in: Bundle.main)
)
stage2to3.label = "Split name into firstName/lastName"
stage2to3.willMigrateHandler = { migrationManager, currentStage in
guard let container = migrationManager.container else { return }
let context = container.newBackgroundContext()
try context.performAndWait {
// Transform data between schema versions
let request = NSFetchRequest<NSManagedObject>(entityName: "Person")
let people = try context.fetch(request)
for person in people {
let fullName = person.value(forKey: "name") as? String ?? ""
let parts = fullName.split(separator: " ", maxSplits: 1)
person.setValue(String(parts.first ?? ""), forKey: "firstName")
person.setValue(parts.count > 1 ? String(parts.last!) : "", forKey: "lastName")
}
try context.save()
}
}
// Apply to the persistent store
let manager = NSStagedMigrationManager([stage1to2, stage2to3])
let description = NSPersistentStoreDescription()
description.setOption(manager,
forKey: NSPersistentStoreStagedMigrationManagerOptionKey)
container.persistentStoreDescriptions = [description]
container.loadPersistentStores { _, error in
if let error { fatalError("Migration failed: \(error)") }
}NSInferMappingModelAutomaticallyOptionNSLightweightMigrationStage[String]Codableimport CoreData
import Testing
struct CoreDataTests {
func makeTestContainer() throws -> NSPersistentContainer {
let container = NSPersistentContainer(name: "MyAppModel")
let description = NSPersistentStoreDescription()
description.type = NSInMemoryStoreType
container.persistentStoreDescriptions = [description]
var loadError: Error?
container.loadPersistentStores { _, error in loadError = error }
if let loadError { throw loadError }
return container
}
@Test func createAndFetchTrip() throws {
let container = try makeTestContainer()
let context = container.viewContext
let trip = CDTrip(context: context)
trip.name = "Test Trip"
trip.startDate = .now
try context.save()
let request: NSFetchRequest<CDTrip> = CDTrip.fetchRequest()
let trips = try context.fetch(request)
#expect(trips.count == 1)
#expect(trips.first?.name == "Test Trip")
}
}NSManagedObjectModelprivate let sharedModel: NSManagedObjectModel = {
let url = Bundle.main.url(forResource: "MyAppModel", withExtension: "momd")!
return NSManagedObjectModel(contentsOf: url)!
}()
func makeTestContainer() throws -> NSPersistentContainer {
let container = NSPersistentContainer(name: "MyAppModel",
managedObjectModel: sharedModel)
// ... configure in-memory store
}| Mistake | Fix |
|---|---|
Passing | Pass |
| Forgetting to merge batch operation results | Call |
Calling | Guard with |
Using deprecated | Use |
Not setting | Set |
Modifying fetch request on live | Call |
| Batch delete ignoring Deny delete rule | Batch delete bypasses delete rules; validate manually |
Marking | Do not. Pass |
NSPersistentContainerviewContextperform(_:)performAndWait(_:)automaticallyMergesChangesFromParentviewContextmergePolicyviewContextNSFetchedResultsControllerNSManagedObjectModelNSManagedObject