top of page

AsyncStream... The `QuakeMonitor` Example

On a Monday morning, I devoted my reading time to bridging a particular gap.


I've designed several bridge classes for certain Apple Services/frameworks, such as the Authorization Service, which aren't yet compatible with the new async/await structure.


If you haven't delved into SE-0300, I highly recommend you do. It discusses bridges that allow callbacks, delegates, and functions (from before the advent of the async/await structure) to be compatible with the new async/await syntax.


I'll be highlighting how these bridges work for both single and multiple-value implementations.


Single callback-delegate pattern


I'll demonstrate a class that I anticipate will be called only once and will return just one value. It won't be invoked again unless the user uninstalls the iOS application from their device. For this illustration, I'll employ withCheckedThrowingContinuation, one of Continuation's variants.

class AuthorizationService: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {

public func signInWithApple() async throws -> ASAuthorizationAppleIDCredential {
....
return try await withCheckedThrowingContinuation { continuation in
            self.activeContinuation = continuation
            controller.performRequests()
        }
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        self.activeContinuation?.resume(throwing: error)
        self.activeContinuation = nil
    }
func authorizationController(controller: ASAuthorizationController, 
didCompleteWithAuthorization authorization: ASAuthorization) {
...
  self.activeContinuation?.resume(returning: authorization)
...
}


Values over time

After studying the AsyncStream proposals (SE-0314) and the Swift Async Algorithms blog post, which hinted at conveying values over time using the Continuation* strategy, I became curious about how to implement it and experience its effects in depth. I couldn't find any existing examples that mirrored the most common use cases, so I decided to write one myself. Here it is:


For this example, I'm using CoreLocation and CLHeading, which, from its documentation:

Represents a vector pointing to magnetic North constructed from axis component values x, y, and z

So, I am highly confident this was a great sample.


The Continuation structure recipe (regardless of single vs. multi values) is the following:

  1. Maintain an optional instance of the continuation in your delegate class

  2. Use the continuation inside the delegate methods of the framework either to yield/return value/s or terminate/finish with an error.

  3. Initialize and set the continuation to your current continuation. Further, invoke any function needed to start the framework callback.

Step 1.
class CoreLocationService: NSObject, CLLocationManagerDelegate {
    let manager = CLLocationManager()
    var headings: AsyncThrowingStream<CLHeading, Error>?
    var continuation: AsyncThrowingStream<CLHeading, Error>.Continuation?     
}

Step 2.

    func locationManager(_ manager: CLLocationManager, 
    didUpdateHeading newHeading: CLHeading) {
        activeContinuation?.yield(newHeading)
    }
    
    func locationManager(_ manager: CLLocationManager, 
    didFailWithError error: Error) {
        activeContinuation?.finish(throwing: error)
    }

Step 3.

Given that this sample's goal is to have a variable to loop through, I'm setting the visits: AsyncThrowingStream to the callback. The callback stores the continuation previously declared in my delegate class. It invokes the framework to start heading monitoring and additionally setting a callback to when the events terminate, flowing through

override init() {
        super.init()
        manager.delegate = self
        headings = AsyncThrowingStream { continuation in
            activeContinuation = continuation
            
            activeContinuation?.onTermination = { _ in
                manager.stopUpdatingHeading()
            }
            
            manager.startUpdatingHeading()
        }
    }

With the above setup, my domain layer can have the following code, which happens to keep iterating regardless of the last event that invoked it:

 for try await heading in locationService.headings! {
    dump(heading)
 }

bottom of page