.agents/skills/swift-concurrency/references/core-data.md
Use this when:
NSManagedObject instances are crossing context or actor boundaries.@MainActor isolation conflicts with generated NSManagedObject subclasses.Skip this file if:
actors.md.sendable.md.Jump to:
Core Data's thread safety rules don't change with Swift Concurrency:
NSManagedObject between threadsNSManagedObjectID is thread-safe (can pass around)@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
extension NSManagedObjectContext {
func perform<T>(
schedule: ScheduledTaskType = .immediate,
_ block: @escaping () throws -> T
) async rethrows -> T
}
No async alternative for:
func loadPersistentStores(
completionHandler: @escaping (NSPersistentStoreDescription, Error?) -> Void
)
Must bridge manually (see below).
Thread-safe value types representing managed objects.
// 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
}
}
Course Deep Dive: This topic is covered in detail in Lesson 9.2: Sendable and NSManageObjects
Pass only NSManagedObjectID between contexts.
@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()
}
}
// Safe to pass between tasks
let articleID = article.objectID
Task {
await processInBackground(articleID: articleID)
}
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()
Enforce isolation at API level:
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)
}
}
}
// 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()
}
}
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)
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 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()
}
}
perform { }Recommendation: Use simple pattern instead.
When default isolation set to @MainActor, auto-generated managed objects conflict:
// 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
nonisolated: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.
@MainActor
func fetchArticles() throws -> [Article] {
let request = Article.fetchRequest()
return try viewContext.fetch(request)
}
func saveInBackground() async throws {
let context = container.newBackgroundContext()
try await context.perform {
let article = Article(context: context)
article.title = "New Article"
try context.save()
}
}
@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()
}
}
@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)
}
}
@main
struct MyApp: App {
let persistentContainer = NSPersistentContainer(name: "MyApp")
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistentContainer.viewContext)
}
}
}
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 ?? "")
}
}
}
automaticallyMergesChangesFromParent = true// Launch argument
-com.apple.CoreData.ConcurrencyDebug 1
Crashes immediately on thread violations.
Enable in scheme settings to catch data races.
@MainActor
func fetchArticles() -> [Article] {
assert(Thread.isMainThread)
// Fetch from viewContext
}
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
func process(article: Article) async {
// ❌ Article not Sendable
}
func background() async {
let articles = viewContext.fetch(request) // ❌ Not on main thread
}
extension Article: @unchecked Sendable {} // ❌ Doesn't make it safe
func save() async {
backgroundContext.save() // ❌ Not on context's thread
}
NSManagedObject instances across actors: Always transfer NSManagedObjectID or a Sendable value snapshot instead.@unchecked Sendable on NSManagedObject: This does not make it thread-safe. The object is still bound to its context's queue.perform { }: All background context access must go through perform or performAndWait.viewContext from a background task: The view context belongs to the main actor; access it only from @MainActor-isolated code.For Core Data best practices, migration strategies, and advanced patterns: