Should unapplied method references mix with Strict Concurrency?

I'm adapting my state management library to Swift 6 language mode, and the issue I can't overcome is my use of unapplied method references for a pure swift (no objc runtime) target action pattern which avoids circular retains for you. A simplified example is below.

It's important that the target action pattern is synchronous. I'm aware that making my action handler async will compile and eventually trigger my action handler in a delayed way, but this isn't my desired behavior. For instance, without synchronous updates I can't use this pattern to configure a ViewController fully within viewDidLoad, leading to a flash of unloaded state.

It's also necessary for my state management to run within MainActor. Making the sample code classes that implement the target action pattern nonisolated compiles and works correctly. But my actual implementation depends on being MainActor.

What I don't understand is if a) The inability to use unapplied methods with structured concurrency is temporary and will be fixed or if b) Unapplied methods are incompatible structured concurrency and I should find a different pattern for observing changes.

@MainActor func _main() async throws {
    let db = DB()
    let controller = XController(db: db)
    controller.didLoad()
    db.set("Hello")
    print("exiting main")
}
try await _main()


@MainActor class XController {
    
    let db: DB
    
    init(db: DB) {
        self.db = db
    }
    
    func didLoad() {
        // Error: Call to main actor-isolated instance method 'observe(update:)' in a synchronous nonisolated context
        // Warning: Calls to instance method 'observe(update:)' from outside of its actor context are implicitly asynchronous
        db.subscribe(target: self, action: XController.observe)
    }
    
    func observe(update: String) {
        print("observed \(update)")
    }
}

@MainActor class DB {
    
    var value: String = ""
    weak var target: XController?
    var action: ((XController) -> (String) -> Void)?
    
    func subscribe(target: XController, action: @escaping (XController) -> (String) -> Void) {
        self.target = target
        self.action = action
    }
    
    func broadcast() {
        guard let target else { action = nil; return }
        action?(target)(value)
    }

    func set(_ value: String) {
        self.value = value
        broadcast()
    }
}
1 Like

Just annotating the callback should do the job
As you already annotated the whole db class.
β€˜β€™β€™
action: @MainActor ((XController) -> (String) -> Void)?
β€˜β€™β€™

And

β€˜β€™β€™
func subscribe(target: XController, action: @escaping @MainActor (XController) -> (String) -> Void)
β€˜β€™β€™

Sorry if syntax isn’t perfect, was on the phone.

Good catch, though that doesn't fix the error. I don't think this error is related to the type of the callback since you can trigger the same error with the following in the didLoad method instead of db.subscribe:

let _ = XController.observe

It feels like a bug in Swift Concurrency since copying an unapplied method reference to a variable doesn't call any methods but the error is 'Call to main actor-isolated instance method...'

The simplest reproduction that generates the error in Swift 6 language mode is below, though I wanted to motivate my use of unapplied methods with the more complete original example.

@MainActor func _main() async throws {
    let demo = UnappliedMethodDemo()
    demo.didLoad()
}
try await _main()

@MainActor class UnappliedMethodDemo {
    
    func didLoad() {
        // Call to main actor-isolated instance method 'handler()' in a synchronous nonisolated context
        let _ = UnappliedMethodDemo.handler
    }    
    func handler() {
        print("nothing calls this method")
    }
}