Context
In a Mac app built with Swift 5.x and Xcode 14, I have a controller object. This object has several @Published
properties that are observed by SwiftUI views, so I have placed the object on @MainActor
like this:
@MainActor
final class AppController: NSObject, ObservableObject
{
@Published private(set) var foo: String = ""
@Published private(set) var bar: Int = 0
private func doStuff() {
...
}
}
Problem
This app needs to take certain actions when the Mac goes to sleep, so I subscribe to the appropriate notification in the init()
method, but because AppController
is decorated with @MainActor
, I get this warning:
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"
}
}
So, I attempted to isolate it. But (of course) the compiler has something new to complain about. This time an error:
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
}
}
}
So I did this to solve that:
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()
}
}
}
Question
The last bit produces no compiler errors and seems to work. But I have NO idea if it's correct or best-practice.
I do understand why the compiler is complaining and what it's trying to protect me from, but attempting to adopt Swift Concurrency in an existing project is...painful.
CodePudding user response:
You can use your Task { @MainActor in ... }
pattern, but add the [weak self]
capture list to the Task
:
NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.willSleepNotification,
object: nil,
queue: .main
) { [weak self] note in
Task { @MainActor [weak self] in
self?.doStuff()
}
}
CodePudding user response:
An alternative is to use 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()
}
}
}