Friday, November 14, 2025

NotificationQueue and Custom Dispatch Source Coalescing

I have some old code that uses NSNotificationQueue to coalesce notifications. I think this is an underappreciated class. (Even in the old days, I saw a lot more talk about +cancelPreviousPerformRequestsWithTarget:selector:object:.) My newer code uses more threads, but notification queues are not thread-safe. You can create additional queues beyond the default and have each one be thread-isolated, but the delayed posting is based around the run loop. That doesn’t play so well with GCD and now Swift Testing.

NotificationCenter got some shiny new APIs in Swift 6.2, but there was no love for NotificationQueue. I’m not sure it’s gotten any documented improvements in the entire time I’ve been programming Cocoa.

My first thought was to write a simple helper that would post notifications asynchronously. After enqueueing a posting block, it would suppress any further posts until it observed the arrival of its own notification.

But it turns out that GCD already has a built-in solution to do stuff like this, and it’s probably more efficient. If you create a custom dispatch source, it will automatically coalesce:

To prevent events from becoming backlogged in a dispatch queue, dispatch sources implement an event coalescing scheme. If a new event arrives before the event handler for a previous event has been dequeued and executed, the dispatch source coalesces the data from the new event data with data from the old event. Depending on the type of event, coalescing may replace the old event or update the information it holds.

Using this API looks a little different in Objective-C vs. Swift. In Swift, to set up and receive notifications, you can do something like:

source = DispatchSource.makeUserDataAddSource(queue: .main)
source.setEventHandler() {
    center.post(notification)
}
source.resume()

Note that you can post the notifications on whichever queue you want. The main queue works well for coalescing UI updates.

To enqueue a notification, from any thread, you can just use:

source.add(data: 1)

If you want, within the event handler, you can access source.data to see how many updates were coalesced.

I’m pretty sure this is the most modern way to do event coalescing. I didn’t see anything related in Swift Concurrency, though you could certainly use that to implement your own solution.

Previously:

2 Comments RSS · Twitter · Mastodon


I think AsyncStream offers a Swift Concurrency version of this? When you create the AsyncStream, you can specify a buffering policy (e.g. only retain the single newest item).

It think should be possible make a pretty simple type that pipes calls to a public “post” method through a private AsyncStream, which it then internally consumes that stream to actually post the notifications. (I haven’t played with AsyncStream that much so there could be something I’m missing here.)

However, I don’t think it has any API to tell you how many elements were dropped from the stream.


@Wes Thanks. I’m not very good with Swift Concurrency yet. I guess you would want .bufferingOldest(1) and then Task { @MainActor in … } where the … loops over the stream and posts?

Leave a Comment