smithery/Kaakati

core-data-patterns

Expert Core Data decisions for iOS/tvOS: when Core Data vs alternatives, context architecture for multi-threading, migration strategy selection, and performance optimization trade-offs.

Installation

$ npx skills add smithery/Kaakati --skill core-data-patterns

Summary

  • Expert Core Data decisions for iOS/tvOS: when Core Data vs alternatives, context architecture for multi-threading, migration strategy selection, and performance optimization trade-offs.
  • Use when choosing persistence layer, debugging save failures, or optimizing fetch performance.
  • Trigger keywords: Core Data, NSManagedObject, NSPersistentContainer, NSFetchRequest, FetchRequest, migration, lightweight migration, background context, merge policy, faulting

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery/Kaakati · top by installs.

npx skills add smithery/Kaakati

Browse all from smithery/Kaakati

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Skill metadata

Parsed from SKILL.md frontmatter.

Version3.0.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,993 B
  • docs SUMMARY.md 482 B

History

  1. First recorded snapshot · 0 installs

SKILL.md

Core Data Patterns — Expert Decisions

Expert decision frameworks for Core Data choices. Claude knows NSPersistentContainer and fetch requests — this skill provides judgment calls for when Core Data fits and architecture trade-offs.


Decision Trees

Core Data vs Alternatives

What's your persistence need?
├─ Simple key-value storage
│  └─ UserDefaults or @AppStorage
│     Don't use Core Data for preferences
│
├─ Flat list of Codable objects
│  └─ Is query complexity needed?
│     ├─ NO → File-based (JSON/Plist) or SwiftData
│     └─ YES → Core Data or SQLite
│
├─ Complex relationships + queries
│  └─ How many objects?
│     ├─ < 10,000 → SwiftData (simpler) or Core Data
│     └─ > 10,000 → Core Data (more control)
│
├─ iCloud sync required
│  └─ NSPersistentCloudKitContainer
│     Built-in sync with Core Data
│
└─ Cross-platform (non-Apple)
   └─ SQLite directly or Realm
      Core Data is Apple-only

The trap: Using Core Data for simple lists. If you don't need relationships, queries, or undo, consider simpler options like SwiftData or file storage.

Context Architecture

How many contexts do you need?
├─ Simple app, UI-only operations
│  └─ viewContext only
│     Single context for reads and small writes
│
├─ Background imports/exports
│  └─ viewContext + newBackgroundContext()
│     Background for writes, viewContext for UI
│
├─ Complex with multiple writers
│  └─ Parent-child context hierarchy
│     Rarely needed — adds complexity
│
└─ Sync with server
   └─ Dedicated sync context
      performBackgroundTask for sync operations

Migration Strategy

What changed in your model?
├─ Added optional attribute
│  └─ Lightweight migration (automatic)
│
├─ Renamed attribute/entity
│  └─ Lightweight with mapping model hints
│     Set renaming identifier in model
│
├─ Changed attribute type
│  └─ Depends on conversion possibility
│     Int → String: lightweight
│     String → Date: may need custom
│
├─ Added required attribute (no default)
│  └─ Custom migration required
│     Or add default value to make lightweight
│
└─ Complex schema restructuring
   └─ Staged migration
      Multiple model versions, migrate step by step

Merge Policy Selection

What happens on save conflicts?
├─ UI context always wins
│  └─ NSMergeByPropertyObjectTrumpMergePolicy
│     Most common for view context
│
├─ Store (persisted) always wins
│  └─ NSMergeByPropertyStoreTrumpMergePolicy
│     For background sync contexts
│
├─ Need custom resolution
│  └─ Custom merge policy
│     Complex — avoid if possible
│
└─ Fail on conflict
   └─ NSErrorMergePolicy (default)
      Rarely want this

NEVER Do

Context Management

NEVER use viewContext for heavy operations:

// ❌ Blocks main thread during import
func importUsers(_ data: [UserData]) {
    let context = persistenceController.container.viewContext
    for item in data {
        let user = User(context: context)
        user.name = item.name
    }
    try? context.save()  // UI frozen!
}

// ✅ Use background context
func importUsers(_ data: [UserData]) async throws {
    try await persistenceController.container.performBackgroundTask { context in
        for item in data {
            let user = User(context: context)
            user.name = item.name
        }
        try context.save()
    }
}

NEVER pass NSManagedObjects between contexts:

// ❌ Object belongs to different context — crash or undefined behavior
let user = fetchUser(in: backgroundContext)
viewContext.delete(user)  // Wrong context!

// ✅ Re-fetch in target context using objectID
let user = fetchUser(in: backgroundContext)
let userInViewContext = viewContext.object(with: user.objectID) as! User
viewContext.delete(userInViewContext)

NEVER access managed objects off their context's queue:

// ❌ Thread violation — data corruption possible
let user = fetchUser(in: backgroundContext)
DispatchQueue.main.async {
    print(user.name)  // Accessing background object on main thread!
}

// ✅ Use context.perform for thread-safe access
backgroundContext.perform {
    let user = fetchUser(in: backgroundContext)
    let name = user.name
    DispatchQueue.main.async {
        print(name)  // Safe — using local copy
    }
}

Save Operations

NEVER ignore save errors:

// ❌ Silent data loss
try? context.save()

// ✅ Handle errors properly
do {
    try context.save()
} catch {
    context.rollback()
    Logger.coreData.error("Save failed: \(error)")
    throw error
}

NEVER save after every single change:

// ❌ Performance disaster — disk I/O per object
for item in largeDataset {
    let entity = Entity(context: context)
    entity.value = item
    try context.save()  // 10,000 saves!
}

// ✅ Batch changes, save once (or periodically)
for (index, item) in largeDataset.enumerated() {
    let entity = Entity(context: context)
    entity.value = item

    // Save every 1000 objects to manage memory
    if index % 1000 == 0 {
        try context.save()
        context.reset()  // Release memory
    }
}
try context.save()  // Final batch

NEVER call save on context with no changes:

// ❌ Unnecessary disk I/O
func periodicSave() {
    try? context.save()  // No-op but still has overhead
}

// ✅ Check for changes first
func saveIfNeeded() throws {
    guard context.hasChanges else { return }
    try context.save()
}

Fetch Optimization

NEVER fetch all objects when you need a count:

// ❌ Loads all objects into memory just to count
let users = try context.fetch(User.fetchRequest())
let count = users.count  // May fetch thousands!

// ✅ Use count fetch
let request = User.fetchRequest()
let count = try context.count(for: request)

NEVER fetch everything without limits:

// ❌ May load entire database
let request = User.fetchRequest()
let allUsers = try context.fetch(request)

// ✅ Set appropriate limits
let request = User.fetchRequest()
request.fetchLimit = 50
request.fetchBatchSize = 20  // Loads in batches

NEVER forget to prefetch relationships you'll access:

// ❌ N+1 problem — each post access triggers fault
let request = User.fetchRequest()
let users = try context.fetch(request)
for user in users {
    print(user.posts.count)  // Separate fetch per user!
}

// ✅ Prefetch relationships
let request = User.fetchRequest()
request.relationshipKeyPathsForPrefetching = ["posts"]
let users = try context.fetch(request)

Migration

NEVER assume lightweight migration will work:

// ❌ Crashes on incompatible changes
container.loadPersistentStores { _, error in
    if let error = error {
        fatalError("Failed: \(error)")  // User data lost!
    }
}

// ✅ Handle migration failure gracefully
container.loadPersistentStores { description, error in
    if let error = error as NSError? {
        if error.code == NSMigrationMissingSourceModelError {
            // Offer data reset or crash gracefully
            Self.resetStore()
        }
    }
}

Essential Patterns

Modern Persistence Controller

@MainActor
final class PersistenceController {
    static let shared = PersistenceController()
    static let preview = PersistenceController(inMemory: true)

    let container: NSPersistentContainer

    init(inMemory: Bool = false) {
        container = NSPersistentContainer(name: "Model")

        if inMemory {
            container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
        }

        // Enable lightweight migration
        let description = container.persistentStoreDescriptions.first
        description?.shouldMigrateStoreAutomatically = true
        description?.shouldInferMappingModelAutomatically = true

        container.loadPersistentStores { _, error in
            if let error = error {
                // In production: log and handle gracefully
                fatalError("Core Data load failed: \(error)")
            }
        }

        // View context configuration
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        container.viewContext.undoManager = nil  // Disable if not needed
    }

    func saveViewContext() {
        let context = container.viewContext
        guard context.hasChanges else { return }

        do {
            try context.save()
        } catch {
            Logger.coreData.error("View context save failed: \(error)")
        }
    }
}

Background Import Pattern

extension PersistenceController {
    func importData<T: Decodable>(
        _ items: [T],
        transform: @escaping (T, NSManagedObjectContext) -> Void
    ) async throws {
        try await container.performBackgroundTask { context in
            context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy

            for (index, item) in items.enumerated() {
                transform(item, context)

                // Batch save to manage memory
                if index > 0 && index % 500 == 0 {
                    try context.save()
                    context.reset()
                }
            }

            if context.hasChanges {
                try context.save()
            }
        }
    }
}

// Usage
try await persistenceController.importData(userDTOs) { dto, context in
    let user = User(context: context)
    user.id = dto.id
    user.name = dto.name
}

Efficient Fetch with @FetchRequest

struct UserListView: View {
    // Basic fetch — automatically updates on changes
    @FetchRequest(
        sortDescriptors: [SortDescriptor(\.name)],
        animation: .default
    )
    private var users: FetchedResults<User>

    var body: some View {
        List(users) { user in
            Text(user.name ?? "Unknown")
        }
    }
}

// Dynamic predicate fetch
struct FilteredUserList: View {
    @FetchRequest private var users: FetchedResults<User>

    init(searchText: String) {
        _users = FetchRequest(
            sortDescriptors: [SortDescriptor(\.name)],
            predicate: searchText.isEmpty ? nil : NSPredicate(
                format: "name CONTAINS[cd] %@", searchText
            ),
            animation: .default
        )
    }

    var body: some View {
        List(users) { user in
            Text(user.name ?? "")
        }
    }
}

Quick Reference

Core Data vs Alternatives

Need Solution
Simple preferences UserDefaults
Small Codable lists JSON file or SwiftData
Complex queries + relationships Core Data
iCloud sync NSPersistentCloudKitContainer
Cross-platform SQLite or Realm

Context Types

Context Use For Thread
viewContext UI reads, small writes Main
newBackgroundContext() Heavy writes, imports Background
performBackgroundTask One-off background work Background

Merge Policies

Policy Winner Use Case
ObjectTrump In-memory changes View context
StoreTrump Persisted data Sync context
ErrorMerge Neither (fails) Rarely wanted

Lightweight Migration Support

Change Automatic?
Add optional attribute ✅ Yes
Add attribute with default ✅ Yes
Remove attribute ✅ Yes
Rename (with identifier) ✅ Yes
Change type (compatible) ✅ Maybe
Add required (no default) ❌ No
Change relationship type ❌ No

Red Flags

Smell Problem Fix
viewContext for imports Main thread blocked Use background context
NSManagedObject across contexts Wrong thread access Re-fetch via objectID
try? context.save() Silent data loss Handle errors
Save per object in loop Disk I/O explosion Batch saves
fetch() for count Memory waste context.count(for:)
No fetchLimit Loads entire DB Set reasonable limits
Missing prefetch N+1 fetches relationshipKeyPathsForPrefetching