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() {
...
}
}
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()
}
}
}
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.
In OS versions 26 and above, there is a NotificationCenter.MainActorMessage for notifications that the OS assures will be called on the main actor. For example, in lieu of NSWorkspace.willSleepNotification, you can use:
func registerObserver(for workspace: NSWorkspace = .shared) -> NotificationCenter.ObservationToken {
NotificationCenter.default.addObserver(
of: workspace,
for: .willSleep,
) { [weak self] notification in
self?.doStuff()
}
}
Since NSWorkspace.WillSleepMessage is a MainActorMessage, the using closure is already isolated to the main actor.
For a discussion of this new notification API see WWDC 2025’s What’s new in Swift.
For earlier OS versions, see my original answer below.
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()
}
}
Or, perhaps better, because we know this will be called on the main thread, is to avoid creating a new Task, and instead use MainActor.assumeIsolated {…}:
NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.willSleepNotification,
object: nil,
queue: .main
) { [weak self] note in
MainActor.assumeIsolated {
self?.doStuff()
}
}
This is mentioned in SE-0424:
… if the current thread is not running a task, isolation checking will succeed if the target actor is the
MainActorand the current thread is the main thread.
FWIW, rather than the observer pattern, in Swift concurrency, we can forgo the old completion-handler-based observer, and instead use the asynchronous sequence, 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) { … }
}