Real-World AsyncStream: Location & Downloads

Let's look at real-world AsyncStream patterns for common iOS scenarios.
Follow along with the code: iOS-Practice on GitHub
Download Progress Stream
Track download progress as an async sequence:
class DownloadManager: NSObject, URLSessionDownloadDelegate {
private var continuation: AsyncStream<Double>.Continuation?
func downloadProgress(from url: URL) -> AsyncStream<Double> {
AsyncStream { continuation in
self.continuation = continuation
let session = URLSession(
configuration: .default,
delegate: self,
delegateQueue: nil
)
let task = session.downloadTask(with: url)
task.resume()
continuation.onTermination = { @Sendable _ in
task.cancel()
}
}
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
continuation?.yield(progress)
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
continuation?.yield(1.0)
continuation?.finish()
}
}
Usage:
for await progress in downloadManager.downloadProgress(from: fileURL) {
progressBar.progress = progress
}
print("Download complete!")
Bluetooth Device Discovery
Stream discovered Bluetooth devices:
class BluetoothScanner: NSObject, CBCentralManagerDelegate {
private var centralManager: CBCentralManager!
private var continuation: AsyncStream<CBPeripheral>.Continuation?
var discoveredDevices: AsyncStream<CBPeripheral> {
AsyncStream { continuation in
self.continuation = continuation
self.centralManager = CBCentralManager(delegate: self, queue: nil)
continuation.onTermination = { @Sendable [weak self] _ in
self?.centralManager.stopScan()
}
}
}
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if central.state == .poweredOn {
central.scanForPeripherals(withServices: nil)
}
}
func centralManager(_ central: CBCentralManager,
didDiscover peripheral: CBPeripheral,
advertisementData: [String: Any],
rssi RSSI: NSNumber) {
continuation?.yield(peripheral)
}
}
Keyboard Events
Stream keyboard show/hide notifications:
func keyboardEvents() -> AsyncStream<KeyboardEvent> {
AsyncStream { continuation in
let showObserver = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { notification in
if let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
continuation.yield(.willShow(height: frame.height))
}
}
let hideObserver = NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillHideNotification,
object: nil,
queue: .main
) { _ in
continuation.yield(.willHide)
}
continuation.onTermination = { @Sendable _ in
NotificationCenter.default.removeObserver(showObserver)
NotificationCenter.default.removeObserver(hideObserver)
}
}
}
enum KeyboardEvent {
case willShow(height: CGFloat)
case willHide
}
Motion Updates
Stream accelerometer data:
func accelerometerUpdates() -> AsyncStream<CMAccelerometerData> {
AsyncStream { continuation in
let motionManager = CMMotionManager()
motionManager.accelerometerUpdateInterval = 0.1
motionManager.startAccelerometerUpdates(to: .main) { data, error in
if let data = data {
continuation.yield(data)
}
}
continuation.onTermination = { @Sendable _ in
motionManager.stopAccelerometerUpdates()
}
}
}
The Pattern
All these examples follow the same structure:
- Create the AsyncStream
- Store the continuation
- Setup the underlying system (delegate, observer, etc.)
- Yield values when events occur
- Cleanup in onTermination
Interview Tip
When discussing iOS system APIs, mention that most delegate-based frameworks can be wrapped in AsyncStream. Show you understand the trade-offs: cleaner consumption code, but you're managing the bridge layer.