swiftnotificationcenter

NotificationCenter observer is “mutated after capture by sendable closure”


Consider this simple class:

import Foundation

class ExampleClass {

    init() {
        let notificationCenter = NotificationCenter.default
        var observer: NSObjectProtocol? = nil
        // A warning is emitted for the next line
        observer = notificationCenter.addObserver(
            forName: .NSExtensionHostDidEnterBackground,
            object: nil,
            queue: nil
        ) { [weak self] _ in
            self?.doSomething()
            notificationCenter.removeObserver(observer!)
        }
    }

    func doSomething() {
        print("We got the notification")
    }

}

This code uses the exact pattern that Apple suggests in their documentation for NotificationCenter.addObserver(forName:object:queue:using:), where the NotificationCenter gives us some opaque token that conforms to NSObjectProtocol, and we later use that token to remove the observer.

Recently, though, this code has started to produce a warning. On the line where observer is assigned, the compiler complains that

'observer' mutated after capture by sendable closure

I understand where the compiler is coming from here: if observer is a value type, then the closure will indeed get an “old” version of it. This code does work, though, which suggests to me that either addObserver() returns a reference type, or else that it returns a value-type handle into data that NotificationCenter is storing itself. (It’s unfortunate that Apple doesn’t give us a more specific return type for the method.)

Does this warning indicate an actual problem in this case? If so, what’s the best alternative pattern to use?


Solution

  • You can store the observer on the object itself:

    import Foundation
    
    class ExampleClass {
    
        private var observer: NSObjectProtocol?
    
        init() {
            let notificationCenter = NotificationCenter.default
            observer = notificationCenter.addObserver(
                forName: .NSExtensionHostDidEnterBackground,
                object: nil,
                queue: nil
            ) { [weak self] _ in
                if let self {
                    self.doSomething()
                    notificationCenter.removeObserver(self.observer!)
                }
            }
        }
    
        func doSomething() {
            print("We got the notification")
        }
    
    }
    

    This workaround silences the warning, and it shouldn’t change the semantics of the program in a meaningful way. It’s a bit less elegant, in that a variable that could have stayed local to init (and its closures) is now exposed to the entire class, but it should have the same effect as the previous version of the code.