在使用 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 是......痛苦的。
您可以使用
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) { … }
}
另一种方法是使用
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()
}
}
}