NSObject and Sendable

I’m migrating a NotificationManager class to Swift 6. This class is a small utility wrapper around NotificationCenter and has a method that lets users register a notification that will triggerunless the object is equal to a particular NSObject value.

For example:

manager.registerObserver(for: name, forObject: nil, ignoreIfSentFrom: self) {
  // run this code _only_ if the sender wasn't self 
}

The method looks like this:

  private func registerObserver(_ name: NSNotification.Name, forObject object: AnyObject?,
                                ignoreIfSentFrom ignoredObject: NSObject, block: @Sendable @MainActor @escaping (Notification) -> ())
  {
    let newToken = NotificationCenter.default.addObserver(forName: name, object: object, queue: nil) { note in
      guard (note.object as AnyObject) !== ignoredObject else { return }
      
      Task { @MainActor in
        block(note)
      }
    }
    
    observerTokens.append(newToken)
  }

I get two errors here that I can’t figure out how to resolve:

  • Capture of 'ignoredObject' with non-Sendable type 'NSObject?' in a '@Sendable' closure (on the guard line)
  • Sending 'note' risks causing data races; this is an error in the Swift 6 language mode (for block(note))

Is it still possible to implement this idea with Swift 6 strict concurrency? It looks like Notification is neither Sendable nor @MainActor and since I don’t own that type, I’m at a loss for how to make this work.

1 Like

If you pass .main as the queue instead of nil, the closure will be called on the main thread for you so you don’t need a Task, though you usually need MainActor.assumeIsolated to silence compiler warnings/errors.

2 Likes

I was able to get this to work by using NotificationCenter#notifications inside a MainActor-isolated block like so:

Task { @MainActor in
   for await notification in NotificationCenter.default.notifications(....) { 
   }
}

Double check you are not getting memory leak here – it looks the right candidate for weak self dance:

        for await notification in NotificationCenter.default.notifications(....) {
            guard let self else { break }

Yeah, plus runtime trap if the assumption is wrong.