If I needed to listen for a notification, I’ve always done like so:
NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
But if I move that line within the body of an async method, the compiler suddenly wants that line to be marked with await:
func setup() async {
let importantStuff = await someTask()
// store my important stuff, now setup is complete
// This line won't compile without the await keyword.
// But addObserver is not even an async method 🤨
await NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
// But this works fine
listen()
}
func listen() {
NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
Minimal Example
This code does not compile (compiler wants to add await
on the addObserver call).
class Test {
func thisFailsToCompile() async {
let stuff = try! await URLSession.shared.data(from: URL(string: "https://google.com")!)
NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
}
@objc func foo() {
}
}
What’s going on here?
2
Answers
The compilers wants you to add await because of the notification name. It seems that UIApplication.didEnterBackgroundNotification inside async method requires to await the addObserver method.
It does not occurs with another notifications, but with this type it’s required. It might be because of some Apple implementation.
The problem is that UIApplication is
@MainActor
anddidEnterBackgroundNotification
is a property on it, and it is not markednonisolated
, so it can only be accessed on the main actor. This is likely an oversight by Apple, sincedidEnterBackgroundNotification
is a constant, and should be able to be markednonisolated
. But it’s possibly also a limitation in the ObjC/Swift bridge.There are several fixes for this depending on how you want it to work.
The least-impacting change (other than just keeping the
await
) is to cache the value synchronously:Or, you can mark
Test
as@MainActor
. "UI" things should almost always be@MainActor
, so it depends on what kind of object this is.Or, you can mark just the relevant method as
@MainActor
(though this probably isn’t a great solution for your problem):Note that Notifications are delivered synchronously on the queue that calls
postNotification
. This means that all UIKit (including UIApplication) notifications arrive on the main queue. In fact, most notifications come in on the main queue. This isn’t required or enforced (so local notifications in your app may come in on random queues), but it is very common. Because of this, if you listen to Notifications, it is often very useful to make the object@MainActor
.(But hopefully Apple will also fix this silliness about various constants being isolated.)