Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/core-data.md
1# Core Data and Swift Concurrency23Use this when:45- You need to use Core Data with async/await or actors.6- `NSManagedObject` instances are crossing context or actor boundaries.7- You are resolving default `@MainActor` isolation conflicts with generated NSManagedObject subclasses.89Skip this file if:1011- The issue is general actor isolation, not Core Data specific. Use `actors.md`.12- You need general Sendable guidance. Use `sendable.md`.1314Jump to:1516- Core Principles17- Data Access Objects (DAO) Pattern18- Working Without DAOs (NSManagedObjectID)19- Bridging Closures to Async20- Custom Actor Executor (Advanced)21- Default MainActor Isolation22- SwiftUI Integration23- Common Mistakes2425## Core Principles2627### Thread safety still matters2829Core Data's thread safety rules don't change with Swift Concurrency:30- Can't pass `NSManagedObject` between threads31- Must access objects on their context's thread32- `NSManagedObjectID` is thread-safe (can pass around)3334### NSManagedObject cannot be Sendable3536```swift37@objc(Article)38public class Article: NSManagedObject {39@NSManaged public var title: String // ❌ Mutable, can't be Sendable40}41```4243**Don't use `@unchecked Sendable`** - hides warnings without fixing safety.4445> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.1: An introduction to Swift Concurrency and Core Data](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)4647## Available Async APIs4849### Context perform5051```swift52extension NSManagedObjectContext {53func perform<T>(54schedule: ScheduledTaskType = .immediate,55_ block: @escaping () throws -> T56) async rethrows -> T57}58```5960### What's missing6162No async alternative for:63```swift64func loadPersistentStores(65completionHandler: @escaping (NSPersistentStoreDescription, Error?) -> Void66)67```6869Must bridge manually (see below).7071## Data Access Objects (DAO)7273Thread-safe value types representing managed objects.7475### Pattern7677```swift78// Managed object (not Sendable)79@objc(Article)80public class Article: NSManagedObject {81@NSManaged public var title: String?82@NSManaged public var timestamp: Date?83}8485// DAO (Sendable)86struct ArticleDAO: Sendable, Identifiable {87let id: NSManagedObjectID88let title: String89let timestamp: Date9091init?(managedObject: Article) {92guard let title = managedObject.title,93let timestamp = managedObject.timestamp else {94return nil95}96self.id = managedObject.objectID97self.title = title98self.timestamp = timestamp99}100}101```102103### Benefits104105- **Sendable**: Safe to pass across isolation domains106- **Immutable**: No accidental mutations107- **Clear API**: Explicit data transfer108109### Drawbacks110111- **Requires rewrite**: All fetch/mutation logic112- **Boilerplate**: DAO for each entity113- **Complexity**: Additional layer of abstraction114115> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.2: Sendable and NSManageObjects](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)116117## Working Without DAOs118119Pass only `NSManagedObjectID` between contexts.120121### Basic pattern122123```swift124@MainActor125func fetchArticle(id: NSManagedObjectID) -> Article? {126viewContext.object(with: id) as? Article127}128129func processInBackground(articleID: NSManagedObjectID) async throws {130let backgroundContext = container.newBackgroundContext()131try await backgroundContext.perform {132guard let article = backgroundContext.object(with: articleID) as? Article else {133return134}135// Process article136try backgroundContext.save()137}138}139```140141### NSManagedObjectID is Sendable142143```swift144// Safe to pass between tasks145let articleID = article.objectID146147Task {148await processInBackground(articleID: articleID)149}150```151152## Bridging Closures to Async153154### Load persistent stores155156```swift157extension NSPersistentContainer {158func loadPersistentStores() async throws {159try await withCheckedThrowingContinuation { continuation in160self.loadPersistentStores { description, error in161if let error {162continuation.resume(throwing: error)163} else {164continuation.resume(returning: ())165}166}167}168}169}170171// Usage172try await container.loadPersistentStores()173```174175## Simple CoreDataStore Pattern176177Enforce isolation at API level:178179```swift180nonisolated struct CoreDataStore {181static let shared = CoreDataStore()182183let persistentContainer: NSPersistentContainer184private var viewContext: NSManagedObjectContext {185persistentContainer.viewContext186}187188private init() {189persistentContainer = NSPersistentContainer(name: "MyApp")190persistentContainer.viewContext.automaticallyMergesChangesFromParent = true191192Task { [persistentContainer] in193try? await persistentContainer.loadPersistentStores()194}195}196197// View context operations (main thread)198@MainActor199func perform(_ block: (NSManagedObjectContext) throws -> Void) rethrows {200try block(viewContext)201}202203// Background operations204@concurrent205func performInBackground<T>(206_ block: @escaping (NSManagedObjectContext) throws -> T207) async rethrows -> T {208let context = persistentContainer.newBackgroundContext()209return try await context.perform {210try block(context)211}212}213}214```215216### Usage217218```swift219// Main thread operations220@MainActor221func loadArticles() throws -> [Article] {222try CoreDataStore.shared.perform { context in223let request = Article.fetchRequest()224return try context.fetch(request)225}226}227228// Background operations229func deleteAll() async throws {230try await CoreDataStore.shared.performInBackground { context in231let request = Article.fetchRequest()232let articles = try context.fetch(request)233articles.forEach { context.delete($0) }234try context.save()235}236}237```238239### Why this pattern works240241- **@MainActor**: Enforces view context on main thread242- **@concurrent**: Forces background execution243- **Compile-time safety**: Wrong isolation = error244- **Simple**: No custom executors needed245246## Custom Actor Executor (Advanced)247248**Note**: Usually not needed. Consider simple pattern first.249250> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.3: Using a custom Actor executor for Core Data (advanced)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)251252### Implementation253254```swift255final class NSManagedObjectContextExecutor: @unchecked Sendable, SerialExecutor {256private let context: NSManagedObjectContext257258init(context: NSManagedObjectContext) {259self.context = context260}261262func enqueue(_ job: consuming ExecutorJob) {263let unownedJob = UnownedJob(job)264let executor = asUnownedSerialExecutor()265266context.perform {267unownedJob.runSynchronously(on: executor)268}269}270271func asUnownedSerialExecutor() -> UnownedSerialExecutor {272UnownedSerialExecutor(ordinary: self)273}274}275```276277### Actor usage278279```swift280actor CoreDataStore {281let persistentContainer: NSPersistentContainer282nonisolated let modelExecutor: NSManagedObjectContextExecutor283284nonisolated var unownedExecutor: UnownedSerialExecutor {285modelExecutor.asUnownedSerialExecutor()286}287288private init() {289persistentContainer = NSPersistentContainer(name: "MyApp")290let context = persistentContainer.newBackgroundContext()291modelExecutor = NSManagedObjectContextExecutor(context: context)292}293294func deleteAll<T: NSManagedObject>(295using request: NSFetchRequest<T>296) throws {297let objects = try context.fetch(request)298objects.forEach { context.delete($0) }299try context.save()300}301}302```303304### Drawbacks305306- **Hidden complexity**: Executor details obscure Core Data307- **Forces concurrency**: Even for main thread operations308- **Not simpler**: More code than `perform { }`309- **Error prone**: Easy to use wrong context310311**Recommendation**: Use simple pattern instead.312313## Default MainActor Isolation314315### Problem with auto-generated code316317When default isolation set to `@MainActor`, auto-generated managed objects conflict:318319```swift320// Auto-generated (can't modify)321class Article: NSManagedObject {322// Inherits @MainActor, conflicts with NSManagedObject323}324```325326**Error**: `Main actor-isolated initializer has different actor isolation from nonisolated overridden declaration`327328### Solution: Manual code generation3293301. Set entity to "Manual/None" code generation3312. Generate class definitions3323. Mark as `nonisolated`:333334```swift335nonisolated class Article: NSManagedObject {336@NSManaged public var title: String?337@NSManaged public var timestamp: Date?338}339340> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.4: Autogenerated Core Data Objects and Default MainActor Isolation Conflicts](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)341```342343**Benefit**: Full control over isolation.344345## Common Patterns346347### Fetch on main thread348349```swift350@MainActor351func fetchArticles() throws -> [Article] {352let request = Article.fetchRequest()353return try viewContext.fetch(request)354}355```356357### Background save358359```swift360func saveInBackground() async throws {361let context = container.newBackgroundContext()362try await context.perform {363let article = Article(context: context)364article.title = "New Article"365try context.save()366}367}368```369370### Pass ID, fetch in context371372```swift373@MainActor374func displayArticle(id: NSManagedObjectID) {375guard let article = viewContext.object(with: id) as? Article else {376return377}378// Use article379}380381func processArticle(id: NSManagedObjectID) async throws {382try await CoreDataStore.shared.performInBackground { context in383guard let article = context.object(with: id) as? Article else {384return385}386// Process article387try context.save()388}389}390```391392### Batch operations393394```swift395@concurrent396func deleteAllArticles() async throws {397try await CoreDataStore.shared.performInBackground { context in398let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Article")399let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)400try context.execute(deleteRequest)401}402}403```404405## SwiftUI Integration406407### Environment injection408409```swift410@main411struct MyApp: App {412let persistentContainer = NSPersistentContainer(name: "MyApp")413414var body: some Scene {415WindowGroup {416ContentView()417.environment(\.managedObjectContext, persistentContainer.viewContext)418}419}420}421```422423### View usage424425```swift426struct ContentView: View {427@Environment(\.managedObjectContext) private var viewContext428@FetchRequest(429sortDescriptors: [NSSortDescriptor(keyPath: \Article.timestamp, ascending: true)]430) private var articles: FetchedResults<Article>431432var body: some View {433List(articles) { article in434Text(article.title ?? "")435}436}437}438```439440## Best Practices4414421. **Pass NSManagedObjectID only** - never managed objects4432. **Use perform { }** - don't access context directly4443. **@MainActor for view context** - enforce main thread4454. **@concurrent for background** - force background execution4465. **Manual code generation** - control isolation4476. **Keep it simple** - avoid custom executors unless needed4487. **Enable Core Data debugging** - catch thread violations4498. **Merge changes automatically** - `automaticallyMergesChangesFromParent = true`4509. **Use background contexts** - for heavy operations45110. **Test with Thread Sanitizer** - catch violations early452453## Debugging454455### Enable Core Data concurrency debugging456457```swift458// Launch argument459-com.apple.CoreData.ConcurrencyDebug 1460```461462Crashes immediately on thread violations.463464### Thread Sanitizer465466Enable in scheme settings to catch data races.467468### Assertions469470```swift471@MainActor472func fetchArticles() -> [Article] {473assert(Thread.isMainThread)474// Fetch from viewContext475}476```477478## Decision Tree479480```481Need to access Core Data?482├─ UI/View context?483│ └─ Use @MainActor + viewContext484│485├─ Background operation?486│ ├─ Quick operation? → perform { } on background context487│ └─ Batch operation? → NSBatchDeleteRequest/NSBatchUpdateRequest488│489├─ Pass between contexts?490│ └─ Use NSManagedObjectID only491│492└─ Need Sendable type?493├─ Can refactor? → Use DAO pattern494└─ Can't refactor? → Pass NSManagedObjectID495```496497## Migration Strategy498499### For existing projects5005011. **Enable manual code generation** for all entities5022. **Mark entities as nonisolated** if using default @MainActor5033. **Wrap Core Data access** in CoreDataStore5044. **Use @MainActor** for view context operations5055. **Use @concurrent** for background operations5066. **Pass NSManagedObjectID** between contexts5077. **Test with debugging enabled**508509### For new projects5105111. **Start with simple pattern** (CoreDataStore)5122. **Manual code generation** from the start5133. **Consider DAOs** if heavy cross-context usage5144. **Enable strict concurrency** early515516## Common Mistakes517518### ❌ Passing managed objects519520```swift521func process(article: Article) async {522// ❌ Article not Sendable523}524```525526### ❌ Accessing context from wrong thread527528```swift529func background() async {530let articles = viewContext.fetch(request) // ❌ Not on main thread531}532```533534### ❌ Using @unchecked Sendable535536```swift537extension Article: @unchecked Sendable {} // ❌ Doesn't make it safe538```539540### ❌ Not using perform541542```swift543func save() async {544backgroundContext.save() // ❌ Not on context's thread545}546```547548## Common Mistakes Agents Make549550- **Passing `NSManagedObject` instances across actors**: Always transfer `NSManagedObjectID` or a Sendable value snapshot instead.551- **Using `@unchecked Sendable` on `NSManagedObject`**: This does not make it thread-safe. The object is still bound to its context's queue.552- **Skipping `perform { }`**: All background context access must go through `perform` or `performAndWait`.553- **Accessing `viewContext` from a background task**: The view context belongs to the main actor; access it only from `@MainActor`-isolated code.554555## Further Learning556557For Core Data best practices, migration strategies, and advanced patterns:558- [Core Data Best Practices](https://github.com/avanderlee/CoreDataBestPractices)559- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com)560561