Advanced Concurrency

Bridging Delegates to AsyncStream

July 27, 2026
5 min read
Featured image for blog post: Bridging Delegates to AsyncStream

Many Apple frameworks still use delegates. AsyncStream lets you wrap them into clean async sequences.

Follow along with the code: iOS-Practice on GitHub

The Problem

Delegate code is scattered across methods:

class LocationTracker: NSObject, CLLocationManagerDelegate {
    let manager = CLLocationManager()
    var onLocation: ((CLLocation) -> Void)?

    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        locations.forEach { onLocation?($0) }
    }

    func locationManager(_ manager: CLLocationManager,
                         didFailWithError error: Error) {
        // Handle somewhere else...
    }
}

The Solution

Wrap in AsyncStream:

class AsyncLocationManager: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    private var continuation: AsyncStream<CLLocation>.Continuation?

    var locations: AsyncStream<CLLocation> {
        AsyncStream { continuation in
            self.continuation = continuation
            manager.delegate = self
            manager.startUpdatingLocation()

            continuation.onTermination = { @Sendable [weak self] _ in
                self?.manager.stopUpdatingLocation()
            }
        }
    }

    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        for location in locations {
            continuation?.yield(location)
        }
    }
}

Usage

Now it's simple:

let locationManager = AsyncLocationManager()

for await location in locationManager.locations {
    print("Location: \(location.coordinate)")
}

Key Pattern: Store the Continuation

The delegate methods need access to the continuation:

class DelegateBridge {
    // Store continuation as instance property
    private var continuation: AsyncStream<Event>.Continuation?

    var events: AsyncStream<Event> {
        AsyncStream { continuation in
            self.continuation = continuation
            // Setup...
        }
    }

    // Delegate methods use stored continuation
    func delegateCallback(event: Event) {
        continuation?.yield(event)
    }
}

Thread Safety Considerations

Delegate callbacks might come on any thread. AsyncStream's continuation is thread-safe, but be careful with other state:

// If you need thread-safe state alongside the stream:
actor SafeLocationManager {
    private var lastLocation: CLLocation?

    func updateLocation(_ location: CLLocation) {
        lastLocation = location
    }
}

Complete CLLocationManager Example

class LocationService: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()
    private var continuation: AsyncStream<CLLocation>.Continuation?

    func requestLocation() async throws -> CLLocation {
        // One-shot location request using continuation
        try await withCheckedThrowingContinuation { cont in
            // ... (see Continuations post)
        }
    }

    var locationStream: AsyncStream<CLLocation> {
        AsyncStream { [weak self] continuation in
            guard let self else {
                continuation.finish()
                return
            }

            self.continuation = continuation
            self.manager.delegate = self
            self.manager.requestWhenInUseAuthorization()
            self.manager.startUpdatingLocation()

            continuation.onTermination = { @Sendable _ in
                Task { @MainActor [weak self] in
                    self?.manager.stopUpdatingLocation()
                }
            }
        }
    }

    func locationManager(_ manager: CLLocationManager,
                         didUpdateLocations locations: [CLLocation]) {
        locations.forEach { continuation?.yield($0) }
    }

    func locationManager(_ manager: CLLocationManager,
                         didFailWithError error: Error) {
        // For AsyncThrowingStream, you'd finish with error here
        continuation?.finish()
    }
}

Interview Tip

This pattern shows up constantly in iOS interviews. When asked how to modernize delegate-based code, walk through: store the continuation, yield in delegate callbacks, cleanup in onTermination.

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