← Files Swift ConcurrencyARCHIVED FILE
skills/swift-concurrency/references/core-data.md
14.5 KB · Oct 4, 2026 · 12:30 UTC
# Core Data and Swift Concurrency
Use this when:
- You need to use Core Data with async/await or actors.
- `NSManagedObject` instances are crossing context or actor boundaries.
- You are resolving default `@MainActor` isolation conflicts with generated NSManagedObject subclasses.
Skip this file if:
- The issue is general actor isolation, not Core Data specific. Use `actors.md`.
- You need general Sendable guidance. Use `sendable.md`.
Jump to:
- Core Principles
- Data Access Objects (DAO) Pattern
- Working Without DAOs (NSManagedObjectID)
- Bridging Closures to Async
- Custom Actor Executor (Advanced)
- Default MainActor Isolation
- SwiftUI Integration
- Common Mistakes
## Core Principles
### Thread safety still matters
Core Data's thread safety rules don't change with Swift Concurrency:
- Can't pass `NSManagedObject` between threads
- Must access objects on their context's thread
- `NSManagedObjectID` is thread-safe (can pass around)
### NSManagedObject cannot be Sendable
```swift
@objc(Article)
public class Article: NSManagedObject {
@NSManaged public var title: String // ❌ Mutable, can't be Sendable
}
```
**Don't use `@unchecked Sendable`** - hides warnings without fixing safety.
> **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)
## Available Async APIs
### Context perform
```swift
extension NSManagedObjectContext {
func perform<T>(
schedule: ScheduledTaskType = .immediate,
_ block: @escaping () throws -> T
) async rethrows -> T
}
```
### What's missing
No async alternative for:
```swift
func loadPersistentStores(
completionHandler: @escaping (NSPersistentStoreDescription, Error?) -> Void
)
```
Must bridge manually (see below).
## Data Access Objects (DAO)
Thread-safe value types representing managed objects.
### Pattern
```swift
// Managed object (not Sendable)
@objc(Article)
public class Article: NSManagedObject {
@NSManaged public var title: String?
@NSManaged public var timestamp: Date?
}
// DAO (Sendable)
struct ArticleDAO: Sendable, Identifiable {
let id: NSManagedObjectID
let title: String
let timestamp: Date
init?(managedObject: Article) {
guard let title = managedObject.title,
let timestamp = managedObject.timestamp else {
return nil
}
self.id = managedObject.objectID
self.title = title
self.timestamp = timestamp
}
}
```
### Benefits
- **Sendable**: Safe to pass across isolation domains
- **Immutable**: No accidental mutations
- **Clear API**: Explicit data transfer
### Drawbacks
- **Requires rewrite**: All fetch/mutation logic
- **Boilerplate**: DAO for each entity
- **Complexity**: Additional layer of abstraction
> **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)
## Working Without DAOs
Pass only `NSManagedObjectID` between contexts.
### Basic pattern
```swift
@MainActor
func fetchArticle(id: NSManagedObjectID) -> Article? {
viewContext.object(with: id) as? Article
}
func processInBackground(articleID: NSManagedObjectID) async throws {
let backgroundContext = container.newBackgroundContext()
try await backgroundContext.perform {
guard let article = backgroundContext.object(with: articleID) as? Article else {
return
}
// Process article
try backgroundContext.save()
}
}
```
### NSManagedObjectID is Sendable
```swift
// Safe to pass between tasks
let articleID = article.objectID
Task {
await processInBackground(articleID: articleID)
}
```
## Bridging Closures to Async
### Load persistent stores
```swift
extension NSPersistentContainer {
func loadPersistentStores() async throws {
try await withCheckedThrowingContinuation { continuation in
self.loadPersistentStores { description, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
}
}
}
}
// Usage
try await container.loadPersistentStores()
```
## Simple CoreDataStore Pattern
Enforce isolation at API level:
```swift
nonisolated struct CoreDataStore {
static let shared = CoreDataStore()
let persistentContainer: NSPersistentContainer
private var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
private init() {
persistentContainer = NSPersistentContainer(name: "MyApp")
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
Task { [persistentContainer] in
try? await persistentContainer.loadPersistentStores()
}
}
// View context operations (main thread)
@MainActor
func perform(_ block: (NSManagedObjectContext) throws -> Void) rethrows {
try block(viewContext)
}
// Background operations
@concurrent
func performInBackground<T>(
_ block: @escaping (NSManagedObjectContext) throws -> T
) async rethrows -> T {
let context = persistentContainer.newBackgroundContext()
return try await context.perform {
try block(context)
}
}
}
```
### Usage
```swift
// Main thread operations
@MainActor
func loadArticles() throws -> [Article] {
try CoreDataStore.shared.perform { context in
let request = Article.fetchRequest()
return try context.fetch(request)
}
}
// Background operations
func deleteAll() async throws {
try await CoreDataStore.shared.performInBackground { context in
let request = Article.fetchRequest()
let articles = try context.fetch(request)
articles.forEach { context.delete($0) }
try context.save()
}
}
```
### Why this pattern works
- **@MainActor**: Enforces view context on main thread
- **@concurrent**: Forces background execution
- **Compile-time safety**: Wrong isolation = error
- **Simple**: No custom executors needed
## Custom Actor Executor (Advanced)
**Note**: Usually not needed. Consider simple pattern first.
> **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)
### Implementation
```swift
final class NSManagedObjectContextExecutor: @unchecked Sendable, SerialExecutor {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let executor = asUnownedSerialExecutor()
context.perform {
unownedJob.runSynchronously(on: executor)
}
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}
```
### Actor usage
```swift
actor CoreDataStore {
let persistentContainer: NSPersistentContainer
nonisolated let modelExecutor: NSManagedObjectContextExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
modelExecutor.asUnownedSerialExecutor()
}
private init() {
persistentContainer = NSPersistentContainer(name: "MyApp")
let context = persistentContainer.newBackgroundContext()
modelExecutor = NSManagedObjectContextExecutor(context: context)
}
func deleteAll<T: NSManagedObject>(
using request: NSFetchRequest<T>
) throws {
let objects = try context.fetch(request)
objects.forEach { context.delete($0) }
try context.save()
}
}
```
### Drawbacks
- **Hidden complexity**: Executor details obscure Core Data
- **Forces concurrency**: Even for main thread operations
- **Not simpler**: More code than `perform { }`
- **Error prone**: Easy to use wrong context
**Recommendation**: Use simple pattern instead.
## Default MainActor Isolation
### Problem with auto-generated code
When default isolation set to `@MainActor`, auto-generated managed objects conflict:
```swift
// Auto-generated (can't modify)
class Article: NSManagedObject {
// Inherits @MainActor, conflicts with NSManagedObject
}
```
**Error**: `Main actor-isolated initializer has different actor isolation from nonisolated overridden declaration`
### Solution: Manual code generation
1. Set entity to "Manual/None" code generation
2. Generate class definitions
3. Mark as `nonisolated`:
```swift
nonisolated class Article: NSManagedObject {
@NSManaged public var title: String?
@NSManaged public var timestamp: Date?
}
> **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)
```
**Benefit**: Full control over isolation.
## Common Patterns
### Fetch on main thread
```swift
@MainActor
func fetchArticles() throws -> [Article] {
let request = Article.fetchRequest()
return try viewContext.fetch(request)
}
```
### Background save
```swift
func saveInBackground() async throws {
let context = container.newBackgroundContext()
try await context.perform {
let article = Article(context: context)
article.title = "New Article"
try context.save()
}
}
```
### Pass ID, fetch in context
```swift
@MainActor
func displayArticle(id: NSManagedObjectID) {
guard let article = viewContext.object(with: id) as? Article else {
return
}
// Use article
}
func processArticle(id: NSManagedObjectID) async throws {
try await CoreDataStore.shared.performInBackground { context in
guard let article = context.object(with: id) as? Article else {
return
}
// Process article
try context.save()
}
}
```
### Batch operations
```swift
@concurrent
func deleteAllArticles() async throws {
try await CoreDataStore.shared.performInBackground { context in
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Article")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
try context.execute(deleteRequest)
}
}
```
## SwiftUI Integration
### Environment injection
```swift
@main
struct MyApp: App {
let persistentContainer = NSPersistentContainer(name: "MyApp")
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistentContainer.viewContext)
}
}
}
```
### View usage
```swift
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Article.timestamp, ascending: true)]
) private var articles: FetchedResults<Article>
var body: some View {
List(articles) { article in
Text(article.title ?? "")
}
}
}
```
## Best Practices
1. **Pass NSManagedObjectID only** - never managed objects
2. **Use perform { }** - don't access context directly
3. **@MainActor for view context** - enforce main thread
4. **@concurrent for background** - force background execution
5. **Manual code generation** - control isolation
6. **Keep it simple** - avoid custom executors unless needed
7. **Enable Core Data debugging** - catch thread violations
8. **Merge changes automatically** - `automaticallyMergesChangesFromParent = true`
9. **Use background contexts** - for heavy operations
10. **Test with Thread Sanitizer** - catch violations early
## Debugging
### Enable Core Data concurrency debugging
```swift
// Launch argument
-com.apple.CoreData.ConcurrencyDebug 1
```
Crashes immediately on thread violations.
### Thread Sanitizer
Enable in scheme settings to catch data races.
### Assertions
```swift
@MainActor
func fetchArticles() -> [Article] {
assert(Thread.isMainThread)
// Fetch from viewContext
}
```
## Decision Tree
```
Need to access Core Data?
├─ UI/View context?
│ └─ Use @MainActor + viewContext
│
├─ Background operation?
│ ├─ Quick operation? → perform { } on background context
│ └─ Batch operation? → NSBatchDeleteRequest/NSBatchUpdateRequest
│
├─ Pass between contexts?
│ └─ Use NSManagedObjectID only
│
└─ Need Sendable type?
├─ Can refactor? → Use DAO pattern
└─ Can't refactor? → Pass NSManagedObjectID
```
## Migration Strategy
### For existing projects
1. **Enable manual code generation** for all entities
2. **Mark entities as nonisolated** if using default @MainActor
3. **Wrap Core Data access** in CoreDataStore
4. **Use @MainActor** for view context operations
5. **Use @concurrent** for background operations
6. **Pass NSManagedObjectID** between contexts
7. **Test with debugging enabled**
### For new projects
1. **Start with simple pattern** (CoreDataStore)
2. **Manual code generation** from the start
3. **Consider DAOs** if heavy cross-context usage
4. **Enable strict concurrency** early
## Common Mistakes
### ❌ Passing managed objects
```swift
func process(article: Article) async {
// ❌ Article not Sendable
}
```
### ❌ Accessing context from wrong thread
```swift
func background() async {
let articles = viewContext.fetch(request) // ❌ Not on main thread
}
```
### ❌ Using @unchecked Sendable
```swift
extension Article: @unchecked Sendable {} // ❌ Doesn't make it safe
```
### ❌ Not using perform
```swift
func save() async {
backgroundContext.save() // ❌ Not on context's thread
}
```
## Common Mistakes Agents Make
- **Passing `NSManagedObject` instances across actors**: Always transfer `NSManagedObjectID` or a Sendable value snapshot instead.
- **Using `@unchecked Sendable` on `NSManagedObject`**: This does not make it thread-safe. The object is still bound to its context's queue.
- **Skipping `perform { }`**: All background context access must go through `perform` or `performAndWait`.
- **Accessing `viewContext` from a background task**: The view context belongs to the main actor; access it only from `@MainActor`-isolated code.
## Further Learning
For Core Data best practices, migration strategies, and advanced patterns:
- [Core Data Best Practices](https://github.com/avanderlee/CoreDataBestPractices)
- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com)
SHA-256: c89c4846fa64a2389abf807684ed373a6c0e6d16351dfa051e1fd71b85444d00