Nested method calls in `context.perform` with Swift 6

I'm calling a method with the context as parameter, within the context's perform block – is this really not legal in Swift 6?

actor MyActor {

    func bar(context: NSManagedObjectContext) { /* some code */ }

    func foo(context: NSManagedObjectContext) {
        context.performAndWait {

            self.bar(context: context)
            // WARN: Sending 'context' risks causing data races; this is an error in the Swift 6 language mode
            //   'self'-isolated 'context' is captured by a actor-isolated closure. actor-isolated uses in closure may race against later nonisolated uses
            //   Access can happen concurrently

        }
    }
}

The warning appears when I call a method with a context parameter, within the performAndWait-block.

Background: In my app I have methods that takes in API data, and I need to call the same methods from multiple places with the same context to store it, and I do not want to copy paste the code and have hundreds of lines of duplicate code.

Is there a well-known "this is how you should do it" for situations like this?


This is related to a previous post I made, but it's a bit flimsy and got no response: https://developer.apple.com/forums/thread/770605

Answered by DTS Engineer in 825136022

The compiler gives you a warning / error because NSManagedObjectContext is not Sendable. Swift concurrency check doesn't allow passing an un-sendable type across actors because that can trigger a race.

If you have an actor that interacts with other actos via Core Data objects, consider using NSPersistentContainer and NSManagedObjectID, which are sendable. For example:

actor MyActor {
    private let context: NSManagedObjectContext
    
    init(persistentContainer: NSPersistentContainer) {
        context = persistentContainer.newBackgroundContext()
    }
    
    private func bar(objectID: NSManagedObjectID) {
        context.performAndWait {
            let object = context.object(with: objectID)
            /* Work on object as needed.*/
            print(object)
        }
    }
    
    func foo(objectID: NSManagedObjectID) {
        context.performAndWait {
            self.bar(objectID: objectID)
        }
    }
}

You can then use the actor in the following way:

func runMyActorBar() {
    Task {
        let myActor = MyActor(persistentContainer: PersistenceController.shared.container)
        await myActor.foo(objectID: items[0].objectID)
    }
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

The compiler gives you a warning / error because NSManagedObjectContext is not Sendable. Swift concurrency check doesn't allow passing an un-sendable type across actors because that can trigger a race.

If you have an actor that interacts with other actos via Core Data objects, consider using NSPersistentContainer and NSManagedObjectID, which are sendable. For example:

actor MyActor {
    private let context: NSManagedObjectContext
    
    init(persistentContainer: NSPersistentContainer) {
        context = persistentContainer.newBackgroundContext()
    }
    
    private func bar(objectID: NSManagedObjectID) {
        context.performAndWait {
            let object = context.object(with: objectID)
            /* Work on object as needed.*/
            print(object)
        }
    }
    
    func foo(objectID: NSManagedObjectID) {
        context.performAndWait {
            self.bar(objectID: objectID)
        }
    }
}

You can then use the actor in the following way:

func runMyActorBar() {
    Task {
        let myActor = MyActor(persistentContainer: PersistenceController.shared.container)
        await myActor.foo(objectID: items[0].objectID)
    }
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Nested method calls in `context.perform` with Swift 6
 
 
Q