Consider following code:
class MyViewController: UIViewController
{
var callback: (() -> Void)?
func setCallback(_ callback: (() -> Void)?)
{
self.callback = callback
}
}
class MyClass
{
func foo(myViewController: MyViewController)
{
myViewController.callback = {
self.bar() // Error: "Call to main actor-isolated instance method 'bar()' in a synchronous nonisolated context"
}
myViewController.setCallback {
self.bar() // Warning: "Call to main actor-isolated instance method 'bar()' in a synchronous nonisolated context; this is an error in the Swift 6 language mode"
}
}
@MainActor
func bar()
{}
}
Target build settings (Xcode 16.4):
Swift Language Version = Swift 5
Strict Concurrency Checking = Minimal
Either we set callback closure property directly or with setter function, it's synchronous non-isolated context. So why call to main actor-isolated function gives error in one case but warning in another?
At the very least, you will want to ensure that:
callback closure is defined as isolated to the main actor; andcallback property is properly isolated to the main actor, too.So, let’s tease this out a bit:
You apparently want the closure to be invoked on the main actor. So we would define it as such:
class MyViewController: UIViewController {
var callback: (@MainActor () -> Void)?
func setCallback(_ callback: (@MainActor () -> Void)?) {
self.callback = callback
}
}
Or, I might use a typealias to avoid needing to repeat this @MainActor qualifier in multiple places:
class MyViewController: UIViewController {
typealias Callback = @MainActor () -> Void
var callback: Callback?
func setCallback(_ callback: Callback?) {
self.callback = callback
}
}
You cannot set the actor-isolated callback from a nonisolated context. So you might want to isolate foo to the main actor as well:
class MyClass { // presumably nonisolated unless using Swift 6.2 with the “Default Actor Isolation” build setting of “MainActor”
@MainActor
func foo(myViewController: MyViewController) {
myViewController.callback = {
self.bar() // ✅
}
myViewController.setCallback {
self.bar() // ✅
}
}
@MainActor
func bar() { }
}
Or, you might want to just isolate the entire MyClass to the main actor:
@MainActor
class MyClass {
func foo(myViewController: MyViewController) {
myViewController.callback = {
self.bar() // ✅
}
myViewController.setCallback {
self.bar() // ✅
}
}
func bar() { }
}
While the idea of isolating the entire MyClass to the main actor might seem antithetical to how we traditionally thought about “get everything not UI related off the main thread”, if you watch WWDC 2025’s Embracing Swift concurrency, you will see that Apple is now advocating for a simpler model, which goes under a broad moniker of “Approachable Concurrency”. Namely, we can avoid a lot of multithreaded complexity by isolating more of our code to the main actor, and only introduce the complexities of Sendable, isolation to different actors, etc., as the app requires.
But obviously, the choice is yours. But that video is an interesting watch if you’re interesting in getting your arms around the new paradigm which is being introduced in earnest in Swift 6.2. The basic premise is that while writing our own multithreaded code might be complicated, enjoying Swift concurrency doesn’t have to be.
Yet another approach is to make foo an async function, and then await the call to setCallback:
final class MyClass: Sendable {
func foo(myViewController: MyViewController) async {
await myViewController.setCallback {
self.bar() // ✅
}
}
@MainActor
func bar() { }
}
The trick with this approach is that you have to make MyClass conform to Sendable. Now, as a class without any mutable state, we can just declare it to be final with Sendable conformance and we’re done. If MyClass does have a mutable state, it’s not that easy, and you’d either isolate it to the main actor (as in the prior point), make it an actor, or, worse, add your own synchronization and then adopt the @unchecked Sendable pattern (but only do that if you add your own synchronization).
Bottom line, isolating everything (except any slow and synchronous code you might have) to the main actor simplifies life greatly. But if you really want to have MyClass be nonisolated, you can do that, but it’s just a little more complicated.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With