Persist data with Core Data - models, contexts, fetch requests, migrations, SwiftData
Persist data using Core Data or SwiftData with proper stack setup, model relationships, and background operations. Use when users need to save app data locally, fetch with predicates, or handle migrations.
/plugin marketplace add pluginagentmarketplace/custom-plugin-swift/plugin install swift-assistant@pluginagentmarketplace-swiftThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/DataModel.xcdatamodeld.templateassets/config.yamlreferences/GUIDE.mdscripts/generate-model.shData persistence framework knowledge for Core Data and SwiftData in Apple platforms.
parameters:
framework:
type: string
enum: [core_data, swift_data]
default: swift_data
description: Persistence framework
cloudkit_sync:
type: boolean
default: false
lightweight_migration:
type: boolean
default: true
store_type:
type: string
enum: [sqlite, in_memory, binary]
default: sqlite
| Feature | Core Data | SwiftData |
|---|---|---|
| Min iOS | 3.0+ | 17.0+ |
| Definition | .xcdatamodeld | @Model macro |
| Threading | Manual (contexts) | Actor-based |
| Fetch | NSFetchRequest | #Predicate |
| Learning Curve | Steep | Gentle |
| Component | Purpose |
|---|---|
| NSPersistentContainer | Encapsulates stack |
| NSManagedObjectContext | Working area for objects |
| NSManagedObjectModel | Schema definition |
| NSPersistentStoreCoordinator | Store management |
| Component | Purpose |
|---|---|
| ModelContainer | Schema + store |
| ModelContext | Working area |
| @Model | Entity macro |
| @Query | Fetch in SwiftUI |
import SwiftData
// MARK: - Model Definition
@Model
final class Task {
var title: String
var notes: String
var dueDate: Date?
var isCompleted: Bool
var priority: Priority
var createdAt: Date
@Relationship(deleteRule: .cascade, inverse: \Subtask.parentTask)
var subtasks: [Subtask] = []
@Relationship(inverse: \Tag.tasks)
var tags: [Tag] = []
init(title: String, notes: String = "", dueDate: Date? = nil, priority: Priority = .medium) {
self.title = title
self.notes = notes
self.dueDate = dueDate
self.isCompleted = false
self.priority = priority
self.createdAt = Date()
}
}
@Model
final class Subtask {
var title: String
var isCompleted: Bool
var parentTask: Task?
init(title: String, parentTask: Task? = nil) {
self.title = title
self.isCompleted = false
self.parentTask = parentTask
}
}
@Model
final class Tag {
@Attribute(.unique) var name: String
var color: String
var tasks: [Task] = []
init(name: String, color: String = "#007AFF") {
self.name = name
self.color = color
}
}
enum Priority: String, Codable, CaseIterable {
case low, medium, high, urgent
}
// MARK: - Container Setup
@main
struct TaskApp: App {
let container: ModelContainer
init() {
let schema = Schema([Task.self, Subtask.self, Tag.self])
let config = ModelConfiguration(
schema: schema,
isStoredInMemoryOnly: false,
cloudKitDatabase: .none
)
do {
container = try ModelContainer(for: schema, configurations: config)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(container)
}
}
// MARK: - SwiftUI Integration
struct TaskListView: View {
@Environment(\.modelContext) private var context
@Query(sort: \Task.dueDate) private var tasks: [Task]
var body: some View {
List {
ForEach(tasks) { task in
TaskRow(task: task)
}
.onDelete(perform: deleteTasks)
}
}
private func deleteTasks(at offsets: IndexSet) {
for index in offsets {
context.delete(tasks[index])
}
}
}
// Query with predicate
struct IncompleteTasks: View {
@Query(filter: #Predicate<Task> { !$0.isCompleted },
sort: [SortDescriptor(\Task.priority, order: .reverse)])
private var tasks: [Task]
var body: some View {
List(tasks) { task in
Text(task.title)
}
}
}
import CoreData
// MARK: - Core Data Stack
final class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "DataModel")
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Core Data error: \(error), \(error.userInfo)")
}
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return container
}()
var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func newBackgroundContext() -> NSManagedObjectContext {
let context = persistentContainer.newBackgroundContext()
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
return context
}
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nsError = error as NSError
print("Core Data save error: \(nsError), \(nsError.userInfo)")
}
}
}
}
// MARK: - Fetch Request Helpers
extension NSManagedObjectContext {
func fetch<T: NSManagedObject>(_ type: T.Type, predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil) throws -> [T] {
let request = T.fetchRequest()
request.predicate = predicate
request.sortDescriptors = sortDescriptors
return try fetch(request) as? [T] ?? []
}
}
// MARK: - Background Operations
extension CoreDataStack {
func performBackgroundTask<T>(_ block: @escaping (NSManagedObjectContext) throws -> T) async throws -> T {
try await withCheckedThrowingContinuation { continuation in
persistentContainer.performBackgroundTask { context in
do {
let result = try block(context)
if context.hasChanges {
try context.save()
}
continuation.resume(returning: result)
} catch {
continuation.resume(throwing: error)
}
}
}
}
func batchInsert<T: Encodable>(entities: [T], entityName: String) async throws {
let context = newBackgroundContext()
try await context.perform {
let insertRequest = NSBatchInsertRequest(entityName: entityName, objects: entities.map { entity -> [String: Any] in
let data = try! JSONEncoder().encode(entity)
return try! JSONSerialization.jsonObject(with: data) as! [String: Any]
})
insertRequest.resultType = .count
let result = try context.execute(insertRequest) as! NSBatchInsertResult
print("Inserted \(result.result ?? 0) records")
// Merge changes to view context
NSManagedObjectContext.mergeChanges(
fromRemoteContextSave: [NSInsertedObjectsKey: []],
into: [self.viewContext]
)
}
}
}
// Lightweight Migration (automatic)
let container = NSPersistentContainer(name: "DataModel")
let description = container.persistentStoreDescriptions.first
description?.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description?.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
// Custom Migration Manager
final class MigrationManager {
func requiresMigration(at storeURL: URL, for model: NSManagedObjectModel) -> Bool {
guard let metadata = try? NSPersistentStoreCoordinator.metadataForPersistentStore(ofType: NSSQLiteStoreType, at: storeURL) else {
return false
}
return !model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata)
}
func migrateStore(at storeURL: URL, to destinationModel: NSManagedObjectModel) throws {
// Implementation for custom migration
}
}
| Issue | Cause | Solution |
|---|---|---|
| "The model configuration is invalid" | Schema mismatch | Delete app, check @Model |
| Context save crash | Wrong thread | Use perform/performAndWait |
| Relationship fault | Object deleted | Check for nil before access |
| Slow fetch | Missing index | Add index to frequently queried attributes |
| Migration fails | Non-trivial change | Write custom mapping model |
# Enable Core Data debug logging
# Add to scheme arguments:
-com.apple.CoreData.SQLDebug 1
-com.apple.CoreData.Logging.stderr 1
# SwiftData logging
-com.apple.SwiftData.SQLDebug 1
// Print fetch request SQL
let request: NSFetchRequest<Task> = Task.fetchRequest()
print(request.description)
// Check context state
print("Inserted: \(context.insertedObjects.count)")
print("Updated: \(context.updatedObjects.count)")
print("Deleted: \(context.deletedObjects.count)")
validation:
- rule: background_for_heavy_operations
severity: error
check: Use background context for batch operations
- rule: main_thread_for_ui
severity: error
check: Only access viewContext on main thread
- rule: index_frequently_queried
severity: warning
check: Add indexes to attributes used in predicates
Skill("swift-core-data")
swift-networking - Syncing remote dataswift-swiftui - @Query integrationswift-testing - In-memory stores for testsThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.