NSToolbarDelegate and MainActor

I'm using Swift concurrency in my application. The application creates a window with a toolbar. NSToolbarDelegate must implement

func toolbar(
    _ toolbar: NSToolbar,
    itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
    willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem?

This method has to be nonisolated, otherwise you get a warning

Main actor-isolated instance method 'toolbar(_:itemForItemIdentifier:willBeInsertedIntoToolbar:)' cannot be used to satisfy nonisolated protocol requirement

This method is designed to create a new NSToolbarItem. Apple docs:

Use this method to create new NSToolbarItem objects when the toolbar asks for them.

However, NSToolbarItem is isolated on @MainActor, so you have to call its constructor on @MainActor, which is impossible to do from a nonisolated method and still return a value from that method.

Is this a bug in the current version of AppKit or is there a workaround?

Accepted Reply

Most Apple frameworks, and that includes AppKit, have not been audited for concurrency. So you run into problems where methods like toolbar(_:itemForItemIdentifier:willBeInsertedIntoToolbar:), which is obviously meant to be a main-actor-only thing, confuse the compiler.

Standard practice right now is to import such frameworks using the @preconcurrency attribute.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Replies

Most Apple frameworks, and that includes AppKit, have not been audited for concurrency. So you run into problems where methods like toolbar(_:itemForItemIdentifier:willBeInsertedIntoToolbar:), which is obviously meant to be a main-actor-only thing, confuse the compiler.

Standard practice right now is to import such frameworks using the @preconcurrency attribute.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Unfortunately,

@preconcurrency import AppKit

does nothing to eliminate this warning. But, I got the idea. Thank you.

@eskimo I have a similar situation. I have:

extension OnboardingPageViewControllerDataSource: CLLocationManagerDelegate {
	func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
		self.authorizationStatus = manager.authorizationStatus
	}

where OnboardingPageViewControllerDataSource is on the @MainActor.

Importing @preconcurrency import CoreLocation does not remove the warning, which is Main actor-isolated instance method 'locationManagerDidChangeAuthorization' cannot be used to satisfy nonisolated protocol requirement.

I was trying to solve this warning in this way:

nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
		DispatchQueue.main.async {
			self.authorizationStatus = manager.authorizationStatus
		}
}

but this gives me another weird warning Capture of 'manager' with non-sendable type 'CLLocationManager' in a @Sendable closure which seems incorrect because DispatchQueue.main.async does not have @Sendable attributes.

Anyway, do you think the warning is not going away with your solution for us because it's a still 'work in progress' compiler checks or we are doing something wrong here?

ok, so I have 2 answers and 1 question @AntonL

Answer #1 from an Apple Engineer:

locationManagerDidChangeAuthorization isn’t guaranteed to be called on the MainActor. CoreLocation documentation says that:

Core Location calls the methods of your delegate object using the RunLoop of the thread on which you initialized the CLLocationManager object.

But even if you create the CLLocationManager instance on the main thread, Swift Concurrency system can’t know that at compile time.In this case the method needs to be nonisolated and, if necessary, should start up a task and await a call to a helper method like this:

@MainActor
class MyClass: NSObject, CLLocationManagerDelegate {
    
    var authorizationStatus: CLAuthorizationStatus?
    
    nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
         let newStatus = manager.authorizationStatus
         Task { await self.changeAuthorizationStatus(to: newStatus) }
    }

    func changeAuthorizationStatus(to status: CLAuthorizationStatus) {
        self.authorizationStatus = status
    }
}

You can pass a CLAuthorizationStatus between actors without generating a warning, because CLAuthorizationStatus is Sendable.

Answer #2: My iteration on my previous post:

Manager is not Sendable in my case. But the authorizarionStatus is. So we could write this to please the compiler:

nonisolated func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    let newStatus = manager.authorizationStatus
    DispatchQueue.main.async {
      self.authorizationStatus = status
    }
}

The question: @eskimo

Will my second solution with dispatching to main queue somehow affect the performance?

I find it more readable and more "locally reasoned" than the first one with a helper method.

Will my second solution with dispatching to main queue somehow affect the performance?

It’ll obviously have some effect on performance but will that be a significant effect? Almost certainly not.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"