With Xcode 26 defaultIsolation is set to MainActor. I want to do some Core Data stuff in a background thread like this.
Because everything is a MainActor now, even the generated class by the model Item is now a MainActor, which gives me this warning.
Assuming this is the correct way to handle background tasks with this new defaultIsolation:
What, if anything, can I do about that? Should Core Data adopt nonisolated for its auto generated NSManagedObject classes?
nonisolated
struct BackgroundDataHandler {
@concurrent
func saveItem() async throws {
let context = await PersistenceController.shared.container.newBackgroundContext()
try await context.perform {
let newGame = Item(context: context)
newGame.timestamp = Date.now // Main actor-isolated property 'timestamp' can not be mutated from a nonisolated context; this is an error in the Swift 6 language mode
try context.save()
}
}
}
The @concurrent attribute allows functions on the MainActor to always run in the background in response to the defaultIsolation now being the MainActor for Xcode 26 projects.
As I said, this is the reason that all variables on a NSManagedObject class are now Main Actor-isolated by default. If there's no way around fixing the warning given, which I have no idea about right now, then Apple has to update Core Data, which they have not done yet as of iOS 26 beta 1.
I was under impression that a background execution has nothing to do with main thread. To be honest, what I was expecting from async/await from the beginning, was a syntax sugar over GCD.
That's surely the case looking at the last few years. The annoying part is that this would make Core Data unusable for background tasks, which is kind of bad.
I never liked Core Data anyway. It's just a strange wrapper over SQLite. I think you can find/write better/simpler solutions.
With all due respect to Core Data team of course...
It's just common knowledge to have data organized in tables, and fetch data using SQL queries. I mean, it would be trivial to write your own nonisolated func executeSQL(statement: String) using SQLite API under the hood. Most likely you can find something ready-made like that on GitHub.
It’s already nonisolated, the errors you see is for instances inside your class that is isolated to the main actor. CoreData itself, unfortunately, lacks some of the important annotations for new concurrency and given the amount of effort put into SwiftData (which I don’t quite like in comparison to CoreData), not sure if it will be updated properly.
The most convenient way to work with CoreData in the new concurrency system is via actors with custom executors, where custom executor is defined based on NSManagedObjectContext.
For main thread contexts you can either try to define another custom actor or use nonisolated(unsafe) to avoid unnecessary await, since CoreData itself guarantees main thread here.
I think your understanding of SE-0420 is incorrect. It hasn’t changed behaviour introduced by SE-0338.
However, SE-0461 which did introduce @concurrent attribute, does change semantics of nonisolated functions to sticky with calling actor isolation (and relative attribute to opt-out) in order to simplify work with non-Sendable types. And does so behind feature flag.
Yes, just keep in mind that same rules as previously apply to CoreData context — if it is not on main queue (actor), you need wrap that in perform block. From that perspective, wrapping everything into actors is beneficial — much harder to make a mistake.
When I disable code gen for the managed object class, create it myself and add nonisolated to it, the warning goes away, which makes me think NSManagedObject has no such annotation and now uses MainActor when defaultIsolation it set as such.
I've been running into the same while writing a Core Data module for my Swift Concurrency Course. The only way out at this point is to create managed objects manually and add the nonisolated keyword in front of the class, e.g.:
nonisolated class Article: NSManagedObject { ... }
The most convenient way to work with CoreData in the new concurrency system is via actors with custom executors, where custom executor is defined based on NSManagedObjectContext.
I considered this as well. I even went ahead, made it all work, but concluded that it's a bit overkil. In my opinion, using the asyncperform method on NSManagedObjectContext is more than enough. In fact, a custom actor executor could make things more complicated—nothing is stopping you from using that actor, but calling objects with another managed object context.
Therefore, keeping the perform method close to the NSManagedObject context used seems to be the safest solution.
Yet, you can decide to centralize access to the background context by setting up something like:
nonisolated struct CoreDataStore {
static let shared = CoreDataStore()
let persistentContainer: NSPersistentContainer
private var viewContext: NSManagedObjectContext { persistentContainer.viewContext }
private init() {
persistentContainer = NSPersistentContainer(name: "CoreDataConcurrency")
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
Task { [persistentContainer] in
do {
try await persistentContainer.loadPersistentStores()
} catch {
print("Failed to load persistent store: \(error)")
}
}
}
@MainActor
func perform(_ block: (NSManagedObjectContext) throws -> Void) rethrows {
try block(viewContext)
}
@concurrent func deleteAllAndSave<T: NSManagedObject>(using fetchRequest: NSFetchRequest<T>) async throws {
let backgroundContext = persistentContainer.newBackgroundContext()
try await backgroundContext.perform {
let objects = try backgroundContext.fetch(fetchRequest)
for object in objects {
backgroundContext.delete(object)
}
try backgroundContext.save()
}
}
}
This way, you enforce access to the viewContext on the @MainActor and you define long running tasks as @concurrent to run in the background with their own task background context.
I'm still exploring this matter and I'm about to migrate a large Core Data project again, so I'm open to any other insights and suggestions based on my input!
I must admit that I still don't understand how to use Swift concurrency in regard to long running tasks synchronous functions. There is a contract of always going forward because the concurrency system is cooperative. At the same time any long running synchronous function blocks progress and violates the contract, no?
I recall that someone at this forum advised me not to call long running synchronous functions from an async context, and use Dispatch instead, but this may be a false memory.
Yes, but not for future asynchronous work. Whether occupying a thread for a while is a desirable result is situational. It may or may not be what you wanted, but it's not inherently incorrect. It will always return the thread to the pool once the work is done. Waiting synchronously for future asynchronous work may never recover (i.e. may deadlock), so is incorrect.