Using Core Data context.perform with Swift 6 & Concurrency

Right now I'm in the process of fixing my project up for Swift 6 and around 90% of my concurrency and race condition warnings come from usage of Core Data, mostly context.perform calls to be precise.

To easily update certain aspects of a managed object I used to have extensions that would easily make this possible, such as Item.updateTimestamp(). This was likely always bad, but now Swift 6 complains.

Here's a sample of what I'm talking about.

Core Data's NSManagedObject is a difficult beast for me anyway because of all the things you need to consider when multithreaded. I'm getting to grips with all the Swift 6 changes slowly but here I'm stomped on what to do really if I want to keep my extensions approach. Is there any good solution to keep it working like this or is my proposed solution with the DataManager class the best way?

I guess the worst hack would be this, which would make it work again, but would still be unsafe then.

extension Item: @unchecked Sendable { }

Any help would be appreciated. Thanks!

class TestViewController: UIViewController {
    let item: Item = Item() // NSManagedObject
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        Task {
            // My current way to do general operations on a managed object.
            // Error: Sending 'self.item' risks causing data races
            await item.updateTimestamp()
            
            // Using a seperate class function by passing the object id works, but is 'ugly'.
            await DataManager().updateTimestamp(objectID: item.objectID)
        }
    }
}

extension Item {
    func updateTimestamp() async {
        let context = PersistenceController.shared.container.newBackgroundContext()
        
        await context.perform {
            self.timestamp = .now
            try! context.save()
        }
    }
}

class DataManager {
    func updateTimestamp(objectID: NSManagedObjectID) async {
        let context = PersistenceController.shared.container.newBackgroundContext()
        
        await context.perform {
            let item = context.object(with: objectID) as! Item
            item.timestamp = .now
            try! context.save()
        }
    }
}

But that is actually a correct way to handle this – and always was. NSManagedObject is not tread-safe by design (you can read more in CoreData guides), so it is unsafe to pass them from one isolation to another (and never was!) – these isolations are represented by context objects here. You are trying to pass such object from one context (I assume more likely main-thread context) to another (just created) background context, which is not a correct thing to do in the first place. CoreData programming guide specifically states that you need to pass NSManagedObjectID if you want to work with the object from another context, so the

func updateTimestamp(objectID: NSManagedObjectID) async

is the way to go.


As a side note, I think — and by this I mean I haven't tried this and this is just theoretical — good pattern to work with CoreData's contexts can be actors with custom executors, where executors are driven by the context.perform. They seem to be a perfect fit that also allows to ensure concurrency safety and simplify usage.

1 Like