axiom-core-data

Use when choosing Core Data vs SwiftData, setting up the Core Data stack, modeling relationships, or implementing concurrency patterns - prevents thread-confinement errors and migration crashes

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "axiom-core-data" with this command: npx skills add megastep/codex-skills/megastep-codex-skills-axiom-core-data

Core Data

Overview

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:

  • SwiftData (iOS 17+) — New apps, simpler API, Swift-native
  • Core Data — iOS 16 and earlier, advanced features, existing codebases

Quick Decision Tree

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)

Red Flags

If ANY of these appear, STOP:

  • ❌ "Access managed objects on any thread" — Thread-confinement violation
  • ❌ "Skip migration testing on real device" — Simulator hides schema issues
  • ❌ "Use a singleton context everywhere" — Leads to concurrency crashes
  • ❌ "Force lightweight migration always" — Complex changes need mapping models
  • ❌ "Fetch in view body" — Use @FetchRequest or observe in view model

Core Data Stack Setup

Modern Stack (iOS 10+)

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()
    }
}

CloudKit Integration

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
    }()
}

Concurrency Patterns

The Golden Rule

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
}

Background Processing

// ✅ 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

Async/Await (iOS 15+)

// 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)
    }
}

Relationship Modeling

One-to-Many

// 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)
    }
}

Many-to-Many

// 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
    }
}

Delete Rules

RuleBehaviorUse Case
NullifySet relationship to nilOptional relationships
CascadeDelete related objectsOwned children (User → Posts)
DenyPrevent deletion if related objects existProtect referenced data
No ActionDo nothing (manual cleanup required)Rarely appropriate

Fetching Patterns

SwiftUI Integration

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)
        )
    }
}

Batch Fetching (Avoid N+1)

// ❌ 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
}

Batch Size for Large Datasets

let request = User.fetchRequest()
request.fetchBatchSize = 20  // Load 20 at a time as needed
request.returnsObjectsAsFaults = true  // Default, memory efficient

Schema Migration

Lightweight Migration (Automatic)

Handled automatically for:

  • Adding optional attributes
  • Removing attributes
  • Renaming (with renaming identifier)
  • Adding relationships with optional or default value
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true

When Mapping Model Is Needed

  • Changing attribute types
  • Splitting/merging entities
  • Complex relationship changes
  • Data transformation during migration
// Create mapping model in Xcode:
// File → New → Mapping Model
// Select source and destination models

Migration Testing Checklist

MANDATORY before shipping:

  1. ✓ Test on REAL DEVICE (simulator deletes DB on rebuild)
  2. ✓ Install old version, create data
  3. ✓ Install new version over it
  4. ✓ Verify all data accessible
  5. ✓ Check migration performance (large datasets)

Anti-Patterns

1. Singleton Context for Everything

// ❌ 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
    }
}

2. Fetching in View Body

// ❌ 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
}

3. Ignoring Merge Policy

// ❌ WRONG: No merge policy (conflicts crash)
let context = container.viewContext

// ✅ CORRECT: Define merge behavior
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
context.automaticallyMergesChangesFromParent = true

Performance Tips

  1. Use fetchBatchSize for large result sets
  2. Prefetch relationships that will be accessed
  3. Use background contexts for imports/exports
  4. Batch save — don't save after each insert
  5. Use fetchLimit when only first N results are needed
  6. Profile with SQL debug: -com.apple.CoreData.SQLDebug 1

Pressure Scenarios

Scenario 1: "SwiftData is simpler, let's migrate now"

Situation: 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."

Scenario 2: "Skip migration testing, simulator works"

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."

tvOS

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.

Related Skills

  • axiom-core-data-diag — Debugging migrations, thread errors, N+1 queries
  • axiom-swiftdata — Modern alternative for iOS 17+
  • axiom-database-migration — Safe schema evolution patterns
  • axiom-swift-concurrency — Async/await patterns for Core Data

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

ads-competitor

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ads-meta

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

blog-write

No summary provided by upstream source.

Repository SourceNeeds Review