Delegate callbacks and Combine publishers both model a value that arrives over time, but neither reads as plain sequential code. AsyncSequence lets you consume a stream of values with a for await loop, using the same control flow as any other collection.
#The new way
struct LocationUpdate {
let latitude: Double
let longitude: Double
}
final class LocationTracker {
private var continuation: AsyncStream<LocationUpdate>.Continuation?
lazy var updates: AsyncStream<LocationUpdate> = AsyncStream { continuation in
self.continuation = continuation
continuation.onTermination = { [weak self] _ in
self?.stopTracking()
}
}
func startTracking() {
continuation?.yield(LocationUpdate(latitude: 37.33, longitude: -122.03))
}
func stopTracking() {
continuation?.finish()
}
}
func printUpdates(from tracker: LocationTracker) async {
for await update in tracker.updates {
print("lat: \(update.latitude), lon: \(update.longitude)")
}
print("stream finished")
}
#Why it matters
for awaitcomposes withbreak, earlyreturn, and error handling exactly like a synchronous loopcontinuation.finish()terminates the sequence cleanly so the consuming loop exits instead of hangingAsyncStreambridges any push-based API — delegates, notification observers, socket callbacks — into structured concurrency without Combine as a dependency- Standard sequence operators (
map,filter,prefix) work on async sequences the same way they do onSequence
#Gotcha
Walking away from a for await loop early — via break, return, or the enclosing task being cancelled — does not automatically call onTermination unless the stream's producer is actually torn down; if continuation.yield keeps getting called after nothing is consuming, values are silently dropped once the buffer policy (default .unbounded) is exceeded, or memory grows unbounded if it isn't. Set an explicit bufferingPolicy and rely on onTermination rather than assuming the consumer side cleans up the producer for you.