Reputation: 5779
I have a Mac app that needs to perform an action when the Mac sleeps. To do this, I'm using this "modern" approach to listen for the notification:
@MainActor
final class AppController: NSObject, ObservableObject
{
var sleepTask: Task<Void, Never>? = nil
override init()
{
sleepTask = Task { [weak self] in
for await _ in NSWorkspace.shared.notificationCenter.notifications(named: NSWorkspace.willSleepNotification)
{
self?.doSomething()
}
}
}
}
Xcode 15 beta 8 has introduced a new warning on the for/await
call:
Non-sendable type 'Notification?' returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary
1. Generic enum 'Optional' does not conform to the 'Sendable' protocol (Swift.Optional)
But I'm not using the notification object at all; it's not crossing the actor boundary.
How can I silence this warning? (Other than forcing Optional
to conform to @unchecked Sendable
).
Upvotes: 9
Views: 6871
Reputation: 438122
You said:
How can I silence this warning? (Other than forcing
Optional
to conform to@unchecked Sendable
).
You can:
You can declare Notification
to be Sendable
. E.g.,
extension Notification: @unchecked Sendable { }
This silences the warning, but generally is inadvisable. If a type is not Sendable
, one should not bypass this and retroactively add Sendable
conformance, effectively making promises that you might not be able to keep.
You can also change your sequence to yield something other than a Notification
:
sleepTask = Task { [weak self] in
let sequence = NSWorkspace
.shared
.notificationCenter
.notifications(named: NSWorkspace.willSleepNotification)
.map { _ in () }
for await _ in sequence {
self?.doSomething()
}
}
Upvotes: 8
Reputation: 32879
Let's see first where your problem comes from.
AsyncSequence
requires defining an iterator that has a func next() async -> Notification?
method used by the runtime to know when to end the for loop. Thus, behind the scenes, the for loop behaves more like a while:
var asyncIterator = NSWorkspace.shared.notificationCenter.notifications(named:
NSWorkspace.willSleepNotification).makeAsyncIterator()
while let notification = await asyncIterator.next() {
self?.doSomething()
}
init
inherits the execution context, meaning it will execute on the main actor. This means the notification objects will have to be passed between different concurrent execution contexts, hence the warning.Solutions to fix the warning have already been provided, but since I don't like to write an answer without also giving a possible solution, here is one from me: use a detached task.
sleepTask = Task.detached { [weak self] in
for await _ in NSWorkspace.shared.notificationCenter.notifications(named: NSWorkspace.willSleepNotification)
{
await self?.doSomething()
}
}
A detached task doesn't inherit the current actor execution context, which means the for loop will run in the same execution context as the publisher of the notifications. But, this implies that:
await
the doSomething
call. I don't see this as a problem, au-contraire, it might make things more obvious.AppController
, otherwise the for-loop will run indifinetely. But this is also the case with the "non-detached" task, any unstructured tasks you create will need to be "manually" cancelled, otherwise they will leak resources.Upvotes: 3
Reputation: 535889
This is not what you asked, but you don't need (and shouldn't use) a Task within a Task, and you don't need (and shouldn't use) a reference to your Task. The following will do what you want done:
@MainActor
final class AppController: NSObject, ObservableObject {
override init() {
super.init()
Task { [weak self] in
let center = NSWorkspace.shared.notificationCenter
let willSleep = NSWorkspace.willSleepNotification
for await _ in center.notifications(named: willSleep).map({ _ in }) {
self?.doSomething()
}
}
}
func doSomething() {}
}
Properly speaking, you should also add a break
that cancels the task just in case self
(the AppController) goes out of existence. I take it that it will not go out of existence, but it is best to make a habit of doing things properly:
@MainActor
final class AppController: NSObject, ObservableObject {
override init() {
super.init()
Task { [weak self] in
let center = NSWorkspace.shared.notificationCenter
let willSleep = NSWorkspace.willSleepNotification
for await _ in center.notifications(named: willSleep).map({ _ in }) {
guard let self else { break }
doSomething()
}
}
}
func doSomething() {}
}
Upvotes: 0