npx claudepluginhub charleswiltgen/axiom --plugin axiomThis skill uses the workspace's default tool permissions.
**Core principle**: Core Data is a mature object graph and persistence framework. Use it when needing features SwiftData doesn't support, or when targeting older iOS versions.
Provides Ktor server patterns for routing DSL, plugins (auth, CORS, serialization), Koin DI, WebSockets, services, and testApplication testing.
Conducts multi-source web research with firecrawl and exa MCPs: searches, scrapes pages, synthesizes cited reports. For deep dives, competitive analysis, tech evaluations, or due diligence.
Provides demand forecasting, safety stock optimization, replenishment planning, and promotional lift estimation for multi-location retailers managing 300-800 SKUs.
Core principle: Core Data is a mature object graph and persistence framework. Use it when needing features SwiftData doesn't support, or when targeting older iOS versions.
When to use Core Data vs SwiftData:
Which persistence framework?
├─ Targeting iOS 17+ only?
│ ├─ Simple data model? → SwiftData (recommended)
│ ├─ Need public CloudKit database? → Core Data (SwiftData is private-only)
│ ├─ Need custom migration logic? → Core Data (more control)
│ └─ Existing Core Data app? → Keep Core Data or migrate gradually
│
├─ Targeting iOS 16 or earlier?
│ └─ Core Data (SwiftData unavailable)
│
└─ Need both? → Use Core Data with SwiftData wrapper (advanced)
If ANY of these appear, STOP:
import CoreData
class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "Model")
// Configure for CloudKit if needed
// container.persistentStoreDescriptions.first?.cloudKitContainerOptions =
// NSPersistentCloudKitContainerOptions(containerIdentifier: "iCloud.com.app")
container.loadPersistentStores { description, error in
if let error = error {
// Handle appropriately for production
fatalError("Failed to load store: \(error)")
}
}
// Enable automatic merging
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
persistentContainer.newBackgroundContext()
}
}
import CoreData
class CloudKitStack {
lazy var container: NSPersistentCloudKitContainer = {
let container = NSPersistentCloudKitContainer(name: "Model")
guard let description = container.persistentStoreDescriptions.first else {
fatalError("No store description")
}
// Enable CloudKit sync
description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(
containerIdentifier: "iCloud.com.yourapp"
)
// Enable history tracking for sync
description.setOption(true as NSNumber,
forKey: NSPersistentHistoryTrackingKey)
description.setOption(true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
container.loadPersistentStores { _, error in
if let error = error {
fatalError("CloudKit store failed: \(error)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
return container
}()
}
NEVER pass NSManagedObject across threads. Pass objectID instead.
// ❌ WRONG: Passing object across threads
let user = viewContext.fetch(...) // Main thread
Task.detached {
print(user.name) // CRASH: Wrong thread
}
// ✅ CORRECT: Pass objectID, fetch on target context
let userID = user.objectID
Task.detached {
let bgContext = CoreDataStack.shared.newBackgroundContext()
let user = bgContext.object(with: userID) as! User
print(user.name) // Safe
}
// ✅ CORRECT: Background context for heavy work
func importData(_ items: [ImportItem]) async throws {
let context = CoreDataStack.shared.newBackgroundContext()
try await context.perform {
for item in items {
let entity = Entity(context: context)
entity.configure(from: item)
}
try context.save()
}
}
// Changes automatically merge to viewContext if configured
// Modern async context operations
func fetchUsers() async throws -> [User] {
let context = CoreDataStack.shared.viewContext
return try await context.perform {
let request = User.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
return try context.fetch(request)
}
}
// In User entity
@NSManaged var posts: NSSet?
// Convenience accessors
extension User {
var postsArray: [Post] {
(posts?.allObjects as? [Post]) ?? []
}
func addPost(_ post: Post) {
mutableSetValue(forKey: "posts").add(post)
}
}
// Both sides have NSSet
// User.tags <-> Tag.users
extension User {
func addTag(_ tag: Tag) {
mutableSetValue(forKey: "tags").add(tag)
// Core Data automatically adds to tag.users
}
}
| Rule | Behavior | Use Case |
|---|---|---|
| Nullify | Set relationship to nil | Optional relationships |
| Cascade | Delete related objects | Owned children (User → Posts) |
| Deny | Prevent deletion if related objects exist | Protect referenced data |
| No Action | Do nothing (manual cleanup required) | Rarely appropriate |
struct UserList: View {
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \User.name, ascending: true)],
predicate: NSPredicate(format: "isActive == YES"),
animation: .default
)
private var users: FetchedResults<User>
var body: some View {
List(users) { user in
Text(user.name ?? "Unknown")
}
}
}
// Dynamic predicates
struct FilteredList: View {
@FetchRequest var items: FetchedResults<Item>
init(category: String) {
_items = FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.date, ascending: false)],
predicate: NSPredicate(format: "category == %@", category)
)
}
}
// ❌ WRONG: N+1 queries
let users = try context.fetch(User.fetchRequest())
for user in users {
print(user.posts?.count ?? 0) // Fault fired for each user
}
// ✅ CORRECT: Prefetch relationships
let request = User.fetchRequest()
request.relationshipKeyPathsForPrefetching = ["posts"]
let users = try context.fetch(request)
for user in users {
print(user.posts?.count ?? 0) // Already loaded
}
let request = User.fetchRequest()
request.fetchBatchSize = 20 // Load 20 at a time as needed
request.returnsObjectsAsFaults = true // Default, memory efficient
Handled automatically for:
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
// Create mapping model in Xcode:
// File → New → Mapping Model
// Select source and destination models
MANDATORY before shipping:
// ❌ WRONG: One context for all operations
class DataManager {
let context = CoreDataStack.shared.viewContext
func importInBackground() {
// Using main context on background = crash
for item in largeDataset {
let entity = Entity(context: context)
}
}
}
// ✅ CORRECT: Context per operation type
func importInBackground() {
let bgContext = CoreDataStack.shared.newBackgroundContext()
bgContext.perform {
// Safe background work
}
}
// ❌ WRONG: Fetch on every render
var body: some View {
let users = try? context.fetch(User.fetchRequest()) // Called repeatedly!
List(users ?? []) { ... }
}
// ✅ CORRECT: Use @FetchRequest
@FetchRequest(sortDescriptors: [])
var users: FetchedResults<User>
var body: some View {
List(users) { ... } // Automatic updates
}
// ❌ WRONG: No merge policy (conflicts crash)
let context = container.viewContext
// ✅ CORRECT: Define merge behavior
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true
-com.apple.CoreData.SQLDebug 1Situation: New iOS 17 features available, temptation to migrate mid-project.
Risk: Migration is complex. Mixed Core Data + SwiftData has sharp edges.
Response: "Complete current milestone first. Migration needs dedicated time and testing."
Situation: Schema change tested only in simulator.
Risk: Simulator deletes database on rebuild. Real devices keep persistent data and crash.
Response: "MANDATORY: Test on real device with real data. 15 minutes now prevents production crash."
CoreData + CloudKit is dangerous on tvOS. CloudKit metadata causes significant space inflation in the local store, and tvOS has no persistent local storage — the system deletes Caches (including Application Support) at any time. The inflated store plus random deletion is a worst-case combination.
Recommendation: Use SQLiteData with CloudKit SyncEngine instead for tvOS data persistence. See axiom-tvos for full tvOS storage constraints.
axiom-core-data-diag — Debugging migrations, thread errors, N+1 queriesaxiom-swiftdata — Modern alternative for iOS 17+axiom-database-migration — Safe schema evolution patternsaxiom-swift-concurrency — Async/await patterns for Core Data