Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift Concurrency: error vs warning when accessing main actor-isolated function in closure

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?

like image 762
Varrry Avatar asked Dec 14 '25 02:12

Varrry


1 Answers

At the very least, you will want to ensure that:

  • the callback closure is defined as isolated to the main actor; and
  • the attempt to update the view controller’s callback property is properly isolated to the main actor, too.

So, let’s tease this out a bit:

  1. 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
        }
    }
    
  2. 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() { }
    }
    
  3. 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.

  4. 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.

like image 190
Rob Avatar answered Dec 15 '25 17:12

Rob