1 minute read

We have existing Combine-based code that subscribes to two Notification publishers:

import Combine

var subscriptions = Set<AnyCancellable>()

func subscribeToNotifications() {
    NotificationCenter.default.publisher(for: .NSCalendarDayChanged)
        .sink { notification in
            // Take action
        }
        .store(in: &subscriptions)

    NotificationCenter.default.publisher(for: .NSSystemClockDidChange)
        .sink { notification in
            // Take action
        }
        .store(in: &subscriptions)
}

Or we might have transitioned these subscriptions to AsyncSequences, using structured concurrency:

import SwiftUI

@Observable class Model {
    func subscribeToCalendarNotifications() async {
        for await notification in NotificationCenter.default.notifications(named: .NSCalendarDayChanged) {
            // Take action
        }
    }

    func subscribeToSystemClockNotifications() async {
        for await notification in NotificationCenter.default.notifications(named: .NSSystemClockDidChange) {
            // Take action
        }
    }
}

struct ContentView: View {
    let model: Model

    var body: some View {
        Text("Hello, World!")
            .task {
                await model.subscribeToCalendarNotifications()
            }
            .task {
                await model.subscribeToSystemClockNotifications()
            }
    }
}

If we need to add a third or fourth subscription, this approach starts to become repetitive. Can we collapse the subscriptions into one listening method?

@Observable class Model {
    func subscribeToNotifications() async {
        for await notification in NotificationCenter.default.notifications(named: .NSCalendarDayChanged) {
            // Take action
        }

        // We never reach here
        for await notification in NotificationCenter.default.notifications(named: .NSSystemClockDidChange) {
            // Take action
        }
    }
}

struct ContentView: View {
    let model: Model

    var body: some View {
        Text("Hello, World!")
            .task {
                await model.subscribeToNotifications()
            }
    }
}

This approach does not work, as we will not progress past the first for-await loop until the task has been cancelled.

Provided our subscriptions should have the same lifetime semantics, we can use a TaskGroup to create two structured child tasks:

@Observable class Model {
    func subscribeToNotifications() async {
        await withTaskGroup { group in
            group.addTask {
                for await notification in NotificationCenter.default.notifications(named: .NSCalendarDayChanged) {
                    // Take action
                }
            }
            group.addTask {
                for await notification in NotificationCenter.default.notifications(named: .NSSystemClockDidChange) {
                    // Take action
                }
            }
        }
    }
}

struct ContentView: View {
    let model: Model

    var body: some View {
        Text("Hello, World!")
            .task {
                await model.subscribeToNotifications()
            }
    }
}

If we are targeting iOS 17+ or equivalent platforms, it would be appropriate to use withDiscardingTaskGroup.