1

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

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 Sendablenor @MainActor and since I don’t own that type, I’m at a loss for how to make this work.

1 Answer 1

3
  • You can use the notification center API that returns an AsyncSequence, notifications(named:object:). Then just consume the AsyncSequence in a main actor task.
  • Rather than capturing the unsendable NSObject, capture just its ObjectIdentifier, since that's all you need for the identity comparison.
private func registerObserver(
    _ name: NSNotification.Name,
    ignoreIfSentFrom ignoredObject: NSObject, 
    block: @Sendable @MainActor @escaping (Notification) -> ()
) {
    let ignoredObjectID = ObjectIdentifier(ignoredObject)
    let task = Task { @MainActor in
        let stream = NotificationCenter.default.notifications(named: name, object: nil).filter {
            ($0.object as? NSObject).map(ObjectIdentifier.init) != ignoredObjectID
        }
        for await notification in stream {
            block(notification)
        }
    }

    // the 'task' in this case has a similar role to an observer token
    // you can store it in an array and cancel it once you don't need it anymore
    // observerTokens.append(task)
}

I would also consider simply returning the async sequence to let the caller decide how to consume it.

private func notifications(
    _ name: NSNotification.Name,
    ignoreIfSentFrom ignoredObject: NSObject
) -> some AsyncSequence<Notification, Never> {
    let ignoredObjectID = ObjectIdentifier(ignoredObject)
    return NotificationCenter.default.notifications(named: name, object: nil).filter {
        ($0.object as? NSObject).map(ObjectIdentifier.init) != ignoredObjectID
    }
}

This way you don't force the caller to provide a main actor isolated callback. The caller can decide where it wants to be isolated (if at all).

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks! I'd forgotten about ObjectIdentifier. That resolves the first warning. This function is in a large project, and there are over 100 callers of this - switching to the async sequence version would be a bigger migration - maybe something I can do once Swift 6 is done 😅.
@Bill The second warning should be resolved too? Did the first code snippet (not talking about the version that returns the async sequence) still emit the second warning? You are no longer sending the notification because everything is within Task { @MainActor in ... } now.
Sorry, I should've updated my response. You're right - using a sequence internally in NotificationManager does resolve the issue!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.