What AsyncSequence solves
Some values do not arrive once. They arrive over time: notifications, network updates, progress events, location changes, app lifecycle events or user-driven signals. AsyncSequence gives Swift a native way to model those values using structured concurrency instead of callbacks.
The core idea
An AsyncSequence is like a regular Sequence, but each next value may require waiting. Instead of a normal for loop, you consume it with for await. The loop suspends while waiting for the next element, then resumes when a new value arrives.
NotificationCenter example
A practical iOS example is observing app lifecycle notifications. NotificationCenter exposes an async notifications sequence, so you can consume app events with a regular-looking loop that suspends between values.
final class AppLifecycleObserver {
private var task: Task<Void, Never>?
func start() {
task = Task {
let notifications = NotificationCenter.default.notifications(
named: UIApplication.didBecomeActiveNotification
)
for await notification in notifications {
handleAppDidBecomeActive(notification)
}
}
}
func stop() {
task?.cancel()
task = nil
}
private func handleAppDidBecomeActive(_ notification: Notification) {
// Refresh state, resume timers, or trigger analytics.
}
} Cancellation matters
AsyncSequence fits well with task cancellation. If the Task observing notifications is cancelled, the loop stops. In a view model, this means you can keep a Task reference and cancel it in deinit or when the feature is no longer visible.
@MainActor
final class SessionViewModel: ObservableObject {
private var appActiveTask: Task<Void, Never>?
func bindAppLifecycle() {
appActiveTask = Task {
for await _ in NotificationCenter.default.notifications(
named: UIApplication.didBecomeActiveNotification
) {
await reloadSessionIfNeeded()
}
}
}
deinit {
appActiveTask?.cancel()
}
} Creating your own stream
When you need to bridge delegate callbacks or custom events, AsyncStream is often the simplest tool. You create AsyncStream<Event> { continuation in ... } and call continuation.yield(event) whenever a callback arrives. When the source finishes, call continuation.finish().
enum DownloadEvent {
case progress(Double)
case finished(URL)
}
func downloadEvents() -> AsyncStream<DownloadEvent> {
AsyncStream { continuation in
let downloader = Downloader()
downloader.onProgress = { value in
continuation.yield(.progress(value))
}
downloader.onFinish = { fileURL in
continuation.yield(.finished(fileURL))
continuation.finish()
}
continuation.onTermination = { _ in
downloader.cancel()
}
downloader.start()
}
} Why senior iOS engineers should care
AsyncSequence makes event-driven code easier to test, cancel and compose. It also reduces callback nesting and helps teams express time-based behavior clearly. The important habit is to define ownership: who creates the stream, who consumes it, and when cancellation happens.