From core-data
Core Data development expert grounded in objc.io's "Core Data" book and production patterns. Use this skill whenever working with Core Data, NSManagedObject, NSManagedObjectContext, NSPersistentContainer, NSPersistentCloudKitContainer, NSFetchRequest, NSFetchedResultsController, @FetchRequest, Core Data migrations, predicates, or any iOS/macOS data persistence using Core Data. Also trigger when the user mentions Core Data stack, managed objects, fetch requests, relationships, faulting, batch operations, merge policies, or CloudKit sync with Core Data.
How this skill is triggered — by the user, by Claude, or both
Slash command
/core-data:core-dataThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Based on "Core Data" by Florian Kugler & Daniel Eggert (objc.io) and production patterns from
Based on "Core Data" by Florian Kugler & Daniel Eggert (objc.io) and production patterns from real iOS projects.
Core Data is an object graph manager, not a database. Treat it as such for best performance.
For detailed reference on specific topics, read the corresponding file in references/:
references/stack-and-model.md — Stack setup, managed objects, Managed protocol, relationshipsreferences/fetching-and-performance.md — Fetch requests, faulting, batching, indexes, profilingreferences/saving-and-concurrency.md — Change tracking, saving, conflicts, merge policies, multiple contextsreferences/predicates-and-text.md — NSPredicate patterns, string normalization, text searchreferences/migrations.md — Model versions, lightweight/custom migrations, progressive migrationreferences/production-patterns.md — Patterns extracted from real production projects (Boop & Invoice apps)Never use Xcode codegen. Manual subclasses give full control over access levels, custom types, and factory methods.
@objc(CD_Item)
final class CD_Item: NSManagedObject {
@NSManaged private(set) var id: UUID
@NSManaged private(set) var name: String
@NSManaged private(set) var createdAt: Date
}
Use @NSManaged for Core Data-backed properties. Use private(set) for controlled mutation.
Mark classes final for performance.
This protocol eliminates boilerplate across all entities:
protocol Managed: AnyObject, NSFetchRequestResult {
static var entityName: String { get }
static var defaultSortDescriptors: [NSSortDescriptor] { get }
static var defaultPredicate: NSPredicate { get }
}
extension Managed where Self: NSManagedObject {
static var entityName: String { entity().name! }
static var defaultSortDescriptors: [NSSortDescriptor] { [] }
static var defaultPredicate: NSPredicate { NSPredicate(value: true) }
static var sortedFetchRequest: NSFetchRequest<Self> {
let request = NSFetchRequest<Self>(entityName: entityName)
request.sortDescriptors = defaultSortDescriptors
request.predicate = defaultPredicate
return request
}
static func fetch(
in context: NSManagedObjectContext,
configurationBlock: (NSFetchRequest<Self>) -> Void = { _ in }
) -> [Self] {
let request = NSFetchRequest<Self>(entityName: entityName)
configurationBlock(request)
return (try? context.fetch(request)) ?? []
}
}
extension NSManagedObjectContext {
@discardableResult
func saveOrRollback() -> Bool {
do {
try save()
return true
} catch {
rollback()
return false
}
}
func performChanges(block: @escaping () -> Void) {
perform {
block()
_ = self.saveOrRollback()
}
}
}
Every mutation should be wrapped in performChanges — correct queue + auto-save.
Check in-memory objects first (fast), then fetch from SQLite (slow), then create:
static func findOrCreate(
in context: NSManagedObjectContext,
matching predicate: NSPredicate,
configure: (Self) -> Void
) -> Self {
if let existing = findOrFetch(in: context, matching: predicate) {
return existing
}
let newObject: Self = context.insertObject()
configure(newObject)
return newObject
}
static func materializedObject(
in context: NSManagedObjectContext,
matching predicate: NSPredicate
) -> Self? {
for object in context.registeredObjects where !object.isFault {
guard let result = object as? Self, predicate.evaluate(with: result)
else { continue }
return result
}
return nil
}
A fetch request is a full round trip through the entire stack down to SQLite. Minimize them:
registeredObjects / materializedObject firstfetchBatchSize (e.g., 20) for large result setsreturnsObjectsAsFaults = false when you know you need the data@NSManaged private var primitiveWeightUnit: String
private static let weightUnitKey = "weightUnit"
var weightUnit: WeightUnit {
get {
willAccessValue(forKey: Self.weightUnitKey)
let value = WeightUnit(rawValue: primitiveWeightUnit) ?? .lbs
didAccessValue(forKey: Self.weightUnitKey)
return value
}
set {
willChangeValue(forKey: Self.weightUnitKey)
primitiveWeightUnit = newValue.rawValue
didChangeValue(forKey: Self.weightUnitKey)
}
}
Always wrap primitive access in willAccessValue/didAccessValue (getter) or
willChangeValue/didChangeValue (setter). For complex types, use JSONEncoder/JSONDecoder
to store as Binary Data.
let container = NSPersistentCloudKitContainer(name: "MyApp")
if let description = container.persistentStoreDescriptions.first {
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.example.myapp"
)
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
automaticallyMergesChangesFromParent = true for CloudKit sync changesNSMergeByPropertyObjectTrumpMergePolicy — per-property merge, in-memory winsperform/performAndWait before accessing a context or its objectsobjectID instead// Safe pattern:
let ids = backgroundObjects.map { $0.objectID }
mainContext.perform {
let objects = ids.map { mainContext.object(with: $0) }
}
| Argument | Purpose |
|---|---|
-com.apple.CoreData.SQLDebug 1 | SQL statements + timing |
-com.apple.CoreData.MigrationDebug 1 | Migration diagnostics |
-com.apple.CoreData.ConcurrencyDebug 1 | Threading violations |
| Need | Approach |
|---|---|
| FRC → UIKit table/collection view | NSFetchedResultsController + delegate |
| FRC → SwiftUI | @FetchRequest or FRC + AsyncStream wrapper |
| FRC → ObservableObject | FRC delegate → @Published properties |
| Observe single object changes | NSManagedObjectContextObjectsDidChange notification |
| Merge remote/background changes | .NSManagedObjectContextDidSave notification + mergeChanges |
extension NSManagedObjectContext {
func insertObject<A: NSManagedObject>() -> A where A: Managed {
guard let obj = NSEntityDescription.insertNewObject(
forEntityName: A.entityName, into: self) as? A
else { fatalError("Wrong object type") }
return obj
}
}
@discardableResult
static func create(
in context: NSManagedObjectContext,
configure: (Self) -> Void
) -> Self {
let newObject: Self = context.insertObject()
configure(newObject)
return newObject
}
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: Self.entityName)
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
deleteRequest.resultType = .resultTypeObjectIDs
if let result = try? context.execute(deleteRequest) as? NSBatchDeleteResult,
let objectIDs = result.result as? [NSManagedObjectID] {
let changes = [NSDeletedObjectsKey: objectIDs]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
}
static func make<T: NSFetchRequestResult>(
for frc: NSFetchedResultsController<T>
) -> AsyncStream<Void> {
AsyncStream { continuation in
let delegate = FRCDelegateWrapper { continuation.yield() }
frc.delegate = delegate
try? frc.performFetch()
continuation.yield()
continuation.onTermination = { _ in frc.delegate = nil }
}
}
protocol DelayedDeletable: AnyObject {
var markedForDeletionDate: Date? { get set }
}
extension DelayedDeletable where Self: NSManagedObject {
func markForLocalDeletion() {
markedForDeletionDate = Date()
}
static var notMarkedForDeletionPredicate: NSPredicate {
NSPredicate(format: "%K == NULL", "markedForDeletionDate")
}
}
npx claudepluginhub gloomikon/claude-skills --plugin core-dataProvides expert Core Data guidance for iOS/macOS: stack setup, fetch requests & NSFetchedResultsController, saving/merge conflicts, threading/Swift Concurrency, batch ops/persistent history, migrations, performance, CloudKit sync.
Sets up Core Data for iOS persistence with stacks, SwiftUI @FetchRequest integration, predicates, background contexts, and SwiftData migration.
Build, review, or improve Core Data persistence for apps not using SwiftData. Covers stack setup, concurrency, batch operations, NSFetchedResultsController, persistent history, staged migration, and testing.