Swift 并发:@MainActor 对象上的通知回调

问题描述 投票:0回答:2

背景

在使用 Swift 5.x 和 Xcode 14 构建的 Mac 应用程序中,我有一个控制器对象。这个对象有几个

@Published
属性,可以被 SwiftUI 视图观察到,所以我把这个对象放在
@MainActor
上,如下所示:

@MainActor
final class AppController: NSObject, ObservableObject
{
    @Published private(set) var foo: String = ""
    @Published private(set) var bar: Int = 0

    private func doStuff() {
        ...
    }
}

问题

这个应用程序需要在 Mac 进入睡眠状态时采取某些操作,因此我在

init()
方法中订阅了相应的通知,但由于
AppController
装饰有
@MainActor
,我收到此警告:

override init()
{
    NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] note in
        self?.doStuff()     // "Call to main actor-isolated instance method 'doStuff()' in a synchronous nonisolated context; this is an error in Swift 6"
    }
}

所以,我试图隔离它。但是(当然)编译器有一些新的问题需要抱怨。这次出错了:

override init()
{
    NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] note in
        Task { @MainActor in
            self?.doStuff()    // "Reference to captured var 'self' in concurrently-executing code
        }
    }
}

所以我这样做是为了解决这个问题:

override init()
{
    NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] note in
      
        let JUSTSHUTUP: AppController? = self 
        Task { @MainActor in
            JUSTSHUTUP?.doStuff()
        }
    }
}

问题

最后一位不会产生编译器错误并且似乎可以工作。但我不知道这是否正确或最佳实践。

我确实理解编译器为什么会抱怨以及它试图保护我免受什么影响,但尝试在现有项目中采用 Swift Concurrency 是......痛苦的。

swift async-await appkit swift-concurrency
2个回答
7
投票

您可以使用

Task { @MainActor in ... }
模式,但将
[weak self]
捕获列表添加到
Task
:

NSWorkspace.shared.notificationCenter.addObserver(
    forName: NSWorkspace.willSleepNotification,
    object: nil,
    queue: .main
) { [weak self] note in
    Task { @MainActor [weak self] in
        self?.doStuff()
    }
}

或者,也许更好,因为我们知道这将在主线程上调用,是为了避免创建新的

Task
,而是使用
MainActor.assumeIsolated {…}
:

NSWorkspace.shared.notificationCenter.addObserver(
    forName: NSWorkspace.willSleepNotification,
    object: nil,
    queue: .main
) { [weak self] note in
    MainActor.assumeIsolated {
        self?.doStuff()
    }
}

这在SE-0424中提到过:

…如果当前线程没有运行任务,如果目标 Actor 是

MainActor
并且当前线程是 主线程,隔离检查将会成功。


FWIW,而不是观察者模式,在 Swift 并发中,我们可以放弃旧的基于完成处理程序的观察者,而是使用异步序列,

notifications(named:object:)
:

@MainActor
final class AppController: ObservableObject {
    private var notificationTask: Task<Void, Never>?

    deinit {
        notificationTask?.cancel()
    }

    init() {
        notificationTask = Task { [weak self] in
            let sequence = NSWorkspace.shared.notificationCenter.notifications(named: NSWorkspace.willSleepNotification)

            for await notification in sequence {
                self?.doStuff(with: notification)
            }
        }
    }

    private func doStuff(with notification: Notification) { … }
}

4
投票

另一种方法是使用

Combine

import Combine

@MainActor
final class AppController: NSObject, ObservableObject
{
    @Published private(set) var foo: String = ""
    @Published private(set) var bar: Int = 0
    
    private var cancellable : AnyCancellable?
    
    private func doStuff() {
        //
    }
    
    override init()
    {
        super.init()
        cancellable = NSWorkspace.shared.notificationCenter
            .publisher(for: NSWorkspace.willSleepNotification)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] note in
                self?.doStuff()
            }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.