Best for
- Use when working with NSManagedObject subclasses, NSFetchedResultsController for list-driven UI, NSBatchInsertRequest / NSBatchDeleteRequest / NSBatchUpdateRequest for bulk operations, NSPersistentHistoryChangeRequest f…
dpearson2699/swift-ios-skills/skills/core-data/SKILL.md
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 wh
Decision brief
Build and maintain data persistence using Core Data for apps that have not adopted SwiftData. Covers stack setup, concurrency, batch operations, NSFetchedResultsController, persistent history tracking, staged migration, and testing.
Compatibility matrix
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
Installation
The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill "skills/core-data"Inspect the Agent Skill "core-data" from https://github.com/dpearson2699/swift-ios-skills/blob/90c9573272531337962fbb3505036d61ed23389a/skills/core-data/SKILL.md at commit 90c9573272531337962fbb3505036d61ed23389a. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.
Workflow
NSPersistentContainer encapsulates the Core Data stack.
[ ] NSPersistentContainer is initialized once and shared
Core Data contexts are bound to queues. The viewContext is on the main queue; background contexts operate on private queues.
NSManagedObjectContext.perform(:) has an async throws overload (iOS 15+). Avoid marking NSManagedObject subclasses as Sendable.
Efficiently drives UITableView / UICollectionView from a Core Data fetch request, with built-in change tracking and optional caching.
Permission review
The documentation includes network, browsing, or remote request actions.
let trips = try context.fetch(request)Evidence record
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 85/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 933 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated or reviewed according to the visible evidence level |
Pinned source
Build and maintain data persistence using Core Data for apps that have not adopted SwiftData. Covers stack setup, concurrency, batch operations, NSFetchedResultsController, persistent history tracking, staged migration, and testing.
NSPersistentContainer encapsulates the Core Data stack.
Docs: NSPersistentContainer
import 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()
}
}
For CloudKit sync, use NSPersistentCloudKitContainer instead.
Core Data contexts are bound to queues. The viewContext is on the main queue;
background contexts operate on private queues.
Docs: NSManagedObjectContext
Rules:
perform(_:) or performAndWait(_:) when accessing a context
off its own queue.NSManagedObject instances across context or thread boundaries.
Pass NSManagedObjectID instead and re-fetch.automaticallyMergesChangesFromParent = true on the viewContext.// 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(_:) has an async throws overload
(iOS 15+). Avoid marking NSManagedObject subclasses as Sendable.
func 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
}
Do not use @unchecked Sendable on managed objects. If you need
cross-boundary communication, pass the objectID (which is Sendable)
and re-fetch:
let 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()
}
}
Efficiently drives UITableView / UICollectionView from a Core Data fetch
request, with built-in change tracking and optional caching.
Docs: NSFetchedResultsController
import 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)
}
}
Key points:
deleteCache(withName:) before changing the fetch request predicate or
sort descriptors, or set cacheName to nil.didChangeContentWith:) is available
iOS 13+ and is preferred over the older per-change callbacks.reset(), call performFetch() again.Batch operations execute at the SQL level, bypassing the managed object context. They are fast but don't trigger context notifications automatically.
Docs: NSBatchInsertRequest
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]
)
}
}
}
Docs: NSBatchDeleteRequest
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]
)
}
}
}
Always merge changes back into relevant contexts after batch operations. Batch delete does not enforce the Deny delete rule.
For destructive or retryable batch work, use a proof loop: preflight the predicate and expected count, execute with an object-ID result type, merge IDs into live contexts, refetch, and assert the postcondition. On failure, restore a pristine fixture or prove the operation is idempotent before retrying; never blindly rerun a partially completed batch.
Track store-level changes across targets (app, extensions, widgets) and processes. The core workflow is:
Docs: NSPersistentHistoryChangeRequest
Load persistent-history.md when implementing the store options, observer, token persistence, merge loop, or purge policy.
NSStagedMigrationManager (iOS 17+) sequences schema migrations through
ordered lightweight or custom stages. Stage inputs use compiled model-version
checksums, not model names. Apps supporting systems below iOS 17 need the
lightweight migration or mapping-model path.
Docs: NSStagedMigrationManager
Load staged-migration.md when building the ordered stages, model references, custom handler, and persistent-store option.
iOS 17+ supports composite attributes: groups of sub-attributes on an entity that act as a single logical unit. Define them in the model editor by adding a Composite type attribute and nesting sub-attributes beneath it.
Docs: NSCompositeAttributeDescription
Composite attributes map to Codable structs in SwiftData coexistence
scenarios.
Use the swiftdata skill for Core Data + SwiftData coexistence or migration
implementation. Before handing off, preserve these Core Data boundaries:
@Model classes.@Attribute(originalName:).import 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")
}
}
Tips:
NSManagedObjectModel instance across tests to avoid "duplicate
entity" warnings.private 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 NSManagedObject across threads | Pass objectID and re-fetch in the target context |
| Forgetting to merge batch operation results | Call mergeChanges(fromRemoteContextSave:into:) |
Calling save() without checking hasChanges | Guard with context.hasChanges first |
Using deprecated init(concurrencyType:) confinement type | Use .privateQueueConcurrencyType or .mainQueueConcurrencyType |
Not setting mergePolicy on viewContext | Set NSMergeByPropertyObjectTrumpMergePolicy to avoid conflict crashes |
Modifying fetch request on live NSFetchedResultsController without deleting cache | Call deleteCache(withName:) first or use cacheName: nil |
| Batch delete ignoring Deny delete rule | Batch delete bypasses delete rules; validate manually |
Marking NSManagedObject as @unchecked Sendable | Do not. Pass objectID instead |
| Pointing SwiftData at a fresh store during coexistence | Use the existing store URL and compatible schema when SwiftData should share or migrate Core Data data |
NSPersistentContainer is initialized once and sharedviewContext used only on main queue; background contexts for writesperform(_:) or performAndWait(_:) wraps all off-queue context accessautomaticallyMergesChangesFromParent set on viewContextmergePolicy set on viewContext to prevent conflict crashesNSFetchedResultsController fetch requests have sort descriptorsNSManagedObjectModelNSManagedObject instances cross thread boundariesAlternatives
event4u-app/agent-config
Use when the user says "review the design", "check the UI", or wants a comprehensive UI/UX review. Uses a 7-phase methodology covering interaction, responsiveness, accessibility, and more.
event4u-app/agent-config
Use when writing Playwright E2E tests — browser automation, visual regression testing, Page Objects, fixtures, and reliable test patterns.
JasonColapietro/suede-creator-skills
Design AI evals that catch regressions before users do: rubrics, test cases, failure modes, acceptance gates, and AI-SPEC artifacts.
github/awesome-copilot
This skill enables visual inspection of websites running locally or remotely to identify and fix design issues. Triggers on requests like "review website design", "check the UI", "fix the layout", "find design problems". Detects issues with responsive design, accessibility, visual consistency, and layout breakage, then performs fixes at the source code level.