core-data-patterns

Core Data Patterns — Expert Decisions

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 "core-data-patterns" with this command: npx skills add kaakati/rails-enterprise-dev/kaakati-rails-enterprise-dev-core-data-patterns

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 &#x26;&#x26; 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

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

flutter conventions & best practices

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

getx state management patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

ruby oop patterns

No summary provided by upstream source.

Repository SourceNeeds Review