NotificationCenter observer is “mutated after capture by sendable closure”

1.1k views Asked by At

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?

4

There are 4 answers

0
bdesham On BEST ANSWER

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.

0
vadian On

A modern alternative is a Combine publisher

import Combine

class ExampleClass {
    
    private var subscription : AnyCancellable?
    
    init() {
        subscription = NotificationCenter.default
            .publisher(for: .NSExtensionHostDidEnterBackground)
            .sink { [weak self] _ in
                self?.doSomething()
            }
    }
    
    func doSomething() {
        print("We got the notification")
        subscription?.cancel()
        subscription = nil
    }
}
0
Vlad On

You can make a "container" class (reference type) to store a value of any kind (reference type or value type). Then, instead of using NSObjectProtocol directly, you can use that "container".

Compiler will not display warning anymore, simply because "container" is a reference type.

Here is a Playground demo:

import Foundation
import Cocoa
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

let notificationName = NSNotification.Name("MyNotification")

open class MutableReference<T> {

   public var value: T

   public init(value: T) {
      self.value = value
      print("+ Initialized reference")
   }

   deinit {
      print("~ Destroying reference")
   }
}

class ExampleClass {

   init() {
      let notificationCenter = NotificationCenter.default
      let reference = MutableReference<NSObjectProtocol>(value: NSObject()) // Putting "dummy" value, which will be replaced few lines below
      let observer = notificationCenter.addObserver(forName: notificationName, object: nil, queue: nil) { [weak self] _ in
         self?.doSomething()
         print("Will unsubscribe...")
         notificationCenter.removeObserver(reference.value)
      }
      reference.value = observer
      print("+ Created Example class")
   }

   deinit {
      print("~ Destroying Example class")
   }

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

}

var example: ExampleClass! = ExampleClass()
print("= Will sent notification #1")
NotificationCenter.default.post(name: notificationName, object: nil)
print("= Will sent notification #2")
NotificationCenter.default.post(name: notificationName, object: nil)
example = nil

The output will be:

+ Initialized reference
+ Created Example class
= Will sent notification #1
We got the notification
Will unsubscribe...
~ Destroying reference
= Will sent notification #2
~ Destroying Example class
0
Werner Altewischer On

Using Any instead of NSObjectProtocol as type of the variable seems to get rid of the warning.