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.