swiftswift-concurrency

How to type erase `NotificationCenter.Notifications`?


I want to create an asynchronous sequence that emits whenever I receive a notification for some group of notifications (Xcode 16.1 RC). My thought was that the easiest way to do this is to use the merge() method from Apple's swift-async-algorithms package. My initial naive attempt:

    let note1 = NotificationCenter.default.notifications(named: .noteOne) 
    let note2 = NotificationCenter.default.notifications(named: .noteTwo)

    let merged = merge(note1, note2) // ❗️ Conformance of 'Notification' to 'Sendable' is unavailable

The error makes sense; I know that Notification can't be Sendable because of its userInfo property. So I thought I could just map that to something else using

    let note1 = NotificationCenter.default.notifications(named: .noteOne) 
      .map { _ in () }
    let note2 = NotificationCenter.default.notifications(named: .noteTwo)
      .map { _ in () }

    let merged = merge(note1, note2) // ❗️ Conformance of 'Notification' to 'Sendable' is unavailable

This doesn't work; I get the same compilation error. So... how do I do this?


Solution

  • Adding .map { _ in () } doesn't work because AsyncMapSequence<Base, Transformed> only conforms to Sendable if all of these conditions are met:

    See also the source code.

    In this case, the second condition is not met. Notification does not conform to Sendable.

    From the source code, this Sendable conformance is actually @unchecked. This is because AsyncMapSequence is designed to take non-Sendable closure (it stores this in a stored property), and the compiler can't tell that this is safe.

    In fact, if the closure had been a @Sendable one, then only the first condition is required. Since your closure just maps to (), it is Sendable, so you can write your own AsyncMapSequence that takes a @Sendable closure instead.

    Since your closure is also not async, I have written this SyncMapSequence to make things simple,

    public struct SyncMapSequence<Base, Transformed>: AsyncSequence where Base: AsyncSequence {
        let f: @Sendable (Base.Element) -> Transformed
        let base: Base
        
        public struct AsyncIterator: AsyncIteratorProtocol {
            var baseIterator: Base.AsyncIterator
            let f: @Sendable (Base.Element) -> Transformed
            
            public mutating func next() async throws -> Transformed? {
                try await baseIterator.next().map(f)
            }
            
        }
        
        public func makeAsyncIterator() -> AsyncIterator {
            AsyncIterator(baseIterator: base.makeAsyncIterator(), f: f)
        }
    }
    
    extension SyncMapSequence: Sendable where Base: Sendable {}
    
    extension AsyncSequence {
        func syncMap<Transformed>(transform: @Sendable @escaping (Element) -> Transformed) -> SyncMapSequence<Self, Transformed> {
            SyncMapSequence(f: transform, base: self)
        }
    }
    

    Usage:

    let note1 = NotificationCenter.default.notifications(named: .noteOne)
        .syncMap { _ in () }
    let note2 = NotificationCenter.default.notifications(named: .noteTwo)
        .syncMap { _ in () }
    
    let merged = merge(note1, note2)