Advanced Concurrency

Real-World AsyncStream: Location & Downloads

July 30, 2026
6 min read
Featured image for blog post: 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:

  1. Create the AsyncStream
  2. Store the continuation
  3. Setup the underlying system (delegate, observer, etc.)
  4. Yield values when events occur
  5. 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.

Originally published on pixelper.com

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch