Search code examples
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:

    • Base is Sendable
    • Base.Element is Sendable
    • Transformed is Sendable

    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)