尽管有 @MainActor 注释,但主线程上未触发 UI 更新

lin*_*ram 4 macos appkit actor swift swift-concurrency

我正在注释我的函数,@MainActor以确保可以从任何异步位置安全地调用它以触发 UI 更新。尽管如此,我还是遇到了一个错误,不知何故,UI 更新似乎是在后台线程上尝试的,即使(据我理解)该函数严格绑定到`@MainActor\xc2\xb4.

\n

这是我的代码:

\n
/// Dismisses the popup from its presenting view controller.\n@MainActor public func dismiss() {\n    presentingViewController?.dismiss(self)\n}\n
Run Code Online (Sandbox Code Playgroud)\n

它是从 an 内部调用的,NSViewController它使用 监听某个事件NotificationCenter,然后在以下objc函数中启动解雇:

\n
class MainWindowControllerVC: NSWindowController, NSWindowDelegate {\n  override func windowDidLoad() {\n      NotificationCenter.default.addObserver(self, selector: #selector(self.dismissVCastSharePopup), name: .NOTIF_DISMISS_POPUP, object: nil)\n  }\n\n  @objc private func dismissPopup() {\n      // some other cleanup is happening here\n      popup?.dismiss()\n  }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

我收到以下错误:

\n
*** Terminating app due to uncaught exception \'NSInternalInconsistencyException\', reason: \'NSWindow drag regions should only be invalidated on the Main Thread!\'\n
Run Code Online (Sandbox Code Playgroud)\n

以及以下警告:

\n

在此输入图像描述

\n

有人可以解释一下这怎么可能吗?我在这里误解了什么?如果我将相同的代码包装到 aDispatchQueue.main.async或什至Task\xc2\xa0{ @MainActor () -> Void in ... }我不会收到此错误。它与函数之前的注释特别相关。

\n

Rob*_*Rob 5

太长了;博士

\n

当您将函数隔离到 时@MainActor,仅当您从 Swift 并发上下文调用此方法时才相关。如果您从 Swift 并发系统外部(例如从NotificationCenter)调用它,则@MainActor限定符不起作用。

\n

因此,要么从 Swift 并发上下文中调用这个 actor 隔离函数,要么依赖遗留main队列模式。

\n
\n

有多种方法可以解决这个问题。首先,让我将您的示例重构为 MCVE:

\n
import 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
Run Code Online (Sandbox Code Playgroud)\n

在此输入图像描述

\n

有几种方法可以解决这个问题:

\n
    \n
  1. 我的notificationHandlerdismissVCastSharePopup在你的例子中)不在 Swift 并发上下文中。但我可以通过将调用包装在一个 Swift 并发中Task {\xe2\x80\xa6}

    \n
    @objc nonisolated func notificationHandler(_ notification: Notification) {\n    Task { await checkQueue() }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    请注意,我不仅将调用包装在 a 中Task {\xe2\x80\xa6},而且还添加了一个nonisolated限定符,让编译器知道这是从非隔离上下文调用的。(我认为应该从@objc限定符中推断出这一点,但目前还没有。)

    \n
  2. \n
  3. 或者,您可以退回到遗留模式以确保您位于主线程上,例如基于块的模式,addObserver它允许您指定.main队列:

    \n
    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    \xe2\x80\xa6\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    也许不用说,即使我们@objc在后一个例子中不再使用选择器方法,这个闭包也不会在参与者隔离的上下文中被调用,因此它将忽略该@MainActor属性。上面的模式之所以有效,只是因为我遵循了遗留模式并明确指定了.main队列来处理该观察者。

    \n
  4. \n
\n