Subscribing to multiple AsyncSequences with task groups
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
.