\n
Can somebody please explain how this is possible? What am I misunderstanding here? If I wrap the same code into a DispatchQueue.main.async
or even Task { @MainActor () -> Void in ... }
I don't get this error. It is specifically tied to the annotation before the function.
tl;dr
\nWhen you isolate a function to the @MainActor
, that is only relevant when you call this method from a Swift concurrency context. If you call it from outside the Swift concurrency system (such as from the NotificationCenter
), the @MainActor
qualifier has no effect.
So, either call this actor isolated function from Swift concurrency context, or rely on legacy main
queue patterns.
There are a variety of ways to fix this. First, let me refactor your example into a MCVE:
\nimport Cocoa\nimport os.log\n\nprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ViewController")\n\nextension Notification.Name {\n static let demo = Notification.Name(rawValue: Bundle.main.bundleIdentifier! + ".demo")\n}\n\nclass ViewController: NSViewController {\n deinit {\n NotificationCenter.default.removeObserver(self, name: .demo, object: nil)\n }\n\n override func viewDidLoad() {\n super.viewDidLoad()\n\n addObserver()\n postFromBackground()\n }\n\n func addObserver() {\n logger.debug(#function)\n\n NotificationCenter.default.addObserver(self, selector: #selector(notificationHandler(_:)), name: .demo, object: nil)\n }\n\n func postFromBackground() {\n DispatchQueue.global().asyncAfter(deadline: .now() + 1) {\n logger.debug(#function)\n NotificationCenter.default.post(name: .demo, object: nil)\n }\n }\n\n @objc func notificationHandler(_ notification: Notification) {\n checkQueue()\n }\n\n @MainActor func checkQueue() {\n logger.debug(#function)\n dispatchPrecondition(condition: .onQueue(.main))\n logger.debug("OK")\n }\n}\n
\n\nThere are a few ways of solving this:
\nMy notificationHandler
(dismissVCastSharePopup
in your example) is not within a Swift concurrency context. But I can bridge into Swift concurrency by wrapping the the call in a Task {…}
:
@objc nonisolated func notificationHandler(_ notification: Notification) {\n Task { await checkQueue() }\n}\n
\nNote, not only did I wrap the call in a Task {…}
, but I also added a nonisolated
qualifier to let the compiler know that this was being called from a nonisolated context. (Methinks it should infer that from the @objc
qualifier, but it does not currently.)
Alternatively, you can fall back to legacy patterns to make sure you're on the main thread, such as the block-based addObserver
which allows you to specify the .main
queue:
private var observerToken: NSObjectProtocol?\n\nfunc addObserver() {\n observerToken = notificationCenter.addObserver(forName: .demo, object: nil, queue: .main) { [weak self] _ in\n self?.checkQueue()\n }\n\n …\n}\n
\nPerhaps needless to say, even though we are no longer using the @objc
selector method in this latter example, this closure will not be called in an actor isolated context, either, and therefore it will ignore the @MainActor
attribute. The above pattern works solely because I followed legacy patterns and explicitly specified the .main
queue to handle this observer.
Reputation: 1715
I am annotating my function with @MainActor
to ensure that it can be called from any async location safely to trigger a UI Update. Despite, I run into an error where, somehow, the UI Update seems to be attempted on a background thread, even though (to my understanding) the function is strictly bound to the `@MainActor´.
This is my code:
/// Dismisses the popup from its presenting view controller.
@MainActor public func dismiss() {
presentingViewController?.dismiss(self)
}
It is called from inside an NSViewController
which listens to a certain event using NotificationCenter
, after which it initiates dismissal in the following objc
func:
class MainWindowControllerVC: NSWindowController, NSWindowDelegate {
override func windowDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(self.dismissVCastSharePopup), name: .NOTIF_DISMISS_POPUP, object: nil)
}
@objc private func dismissPopup() {
// some other cleanup is happening here
popup?.dismiss()
}
}
I get the following error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
and the following warnings:
Can somebody please explain how this is possible? What am I misunderstanding here? If I wrap the same code into a DispatchQueue.main.async
or even Task { @MainActor () -> Void in ... }
I don't get this error. It is specifically tied to the annotation before the function.
Upvotes: 4
Views: 1503
Reputation: 438417
tl;dr
When you isolate a function to the @MainActor
, that is only relevant when you call this method from a Swift concurrency context. If you call it from outside the Swift concurrency system (such as from the NotificationCenter
), the @MainActor
qualifier has no effect.
So, either call this actor isolated function from Swift concurrency context, or rely on legacy main
queue patterns.
There are a variety of ways to fix this. First, let me refactor your example into a MCVE:
import Cocoa
import os.log
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "ViewController")
extension Notification.Name {
static let demo = Notification.Name(rawValue: Bundle.main.bundleIdentifier! + ".demo")
}
class ViewController: NSViewController {
deinit {
NotificationCenter.default.removeObserver(self, name: .demo, object: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
addObserver()
postFromBackground()
}
func addObserver() {
logger.debug(#function)
NotificationCenter.default.addObserver(self, selector: #selector(notificationHandler(_:)), name: .demo, object: nil)
}
func postFromBackground() {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
logger.debug(#function)
NotificationCenter.default.post(name: .demo, object: nil)
}
}
@objc func notificationHandler(_ notification: Notification) {
checkQueue()
}
@MainActor func checkQueue() {
logger.debug(#function)
dispatchPrecondition(condition: .onQueue(.main))
logger.debug("OK")
}
}
There are a few ways of solving this:
My notificationHandler
(dismissVCastSharePopup
in your example) is not within a Swift concurrency context. But I can bridge into Swift concurrency by wrapping the the call in a Task {…}
:
@objc nonisolated func notificationHandler(_ notification: Notification) {
Task { await checkQueue() }
}
Note, not only did I wrap the call in a Task {…}
, but I also added a nonisolated
qualifier to let the compiler know that this was being called from a nonisolated context. (Methinks it should infer that from the @objc
qualifier, but it does not currently.)
Alternatively, you can fall back to legacy patterns to make sure you're on the main thread, such as the block-based addObserver
which allows you to specify the .main
queue:
private var observerToken: NSObjectProtocol?
func addObserver() {
observerToken = notificationCenter.addObserver(forName: .demo, object: nil, queue: .main) { [weak self] _ in
self?.checkQueue()
}
…
}
Perhaps needless to say, even though we are no longer using the @objc
selector method in this latter example, this closure will not be called in an actor isolated context, either, and therefore it will ignore the @MainActor
attribute. The above pattern works solely because I followed legacy patterns and explicitly specified the .main
queue to handle this observer.
Upvotes: 7