skip to Main Content

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


  1. 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.

    enter image description here

    Login or Signup to reply.
  2. The problem is that UIApplication is @MainActor and didEnterBackgroundNotification is a property on it, and it is not marked nonisolated, so it can only be accessed on the main actor. This is likely an oversight by Apple, since didEnterBackgroundNotification is a constant, and should be able to be marked nonisolated. 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:

    class Test {
        // Cache the value of didEnterBackgroundNotification so that async code doesn't have to access UIApplication
        private let didEnterBackgroundNotification = UIApplication.didEnterBackgroundNotification
    
        func thisFailsToCompile() async {
            NotificationCenter.default.addObserver(self, selector: #selector(foo), name: didEnterBackgroundNotification, object: nil)
        }
    
        @objc func foo() { }
    }
    

    Or, you can mark Test as @MainActor. "UI" things should almost always be @MainActor, so it depends on what kind of object this is.

    @MainActor 
    class Test { ... }
    

    Or, you can mark just the relevant method as @MainActor (though this probably isn’t a great solution for your problem):

        @MainActor func thisFailsToCompile() async {
            NotificationCenter.default.addObserver(self, selector: #selector(foo), name: UIApplication.didEnterBackgroundNotification, object: nil)
        }
    

    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.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search