Skip to content
Back to skills

Core Data Patterns

ASecurity

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

  • 10 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added February 10, 2026
databasesswiftsqldebuggingapidatabaseperformance

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned February 12, 2026

npx -y skills add Kaakati/rails-enterprise-dev --skill core-data-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Core Data Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Core Data Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kaakati-core-data-patterns/badge)](https://www.skillsdirectory.com/skills/kaakati-core-data-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: core-data-patterns
description: "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"
version: "3.0.0"
---

# 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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:
```swift
// ❌ 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

```swift
@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

```swift
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

```swift
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 |

Files in this skill

  • SKILL.md12.7 KB
  • references/complete-persistence-layer.md35.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.