AsyncStream... The `QuakeMonitor` Example

This was a Monday morning reading that I was dedicated to fill the gap.


I have create a couple bridge classes on some of the Apple Services/frameworks like the Authorization Service that are not yet compatible with the new async/await structure.


If you haven't read SE-0300 go read it :) They are these bridges to make callbacks, delegates, and functions (previous to async/await structure) to work with the new async/await syntax structure.


I will showcase these bridges for single and multiple value implementations


Single callback-delegate pattern


I will show the following class that I only expected to call once* and to receive one value*, it will no longer be invoke again unless the user removes the iOS application from their device. For this example I will use withCheckedThrowingContinuation, which is one of the types of Continuations


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 reading the AsyncStream proposals (SE-0314) and the Swift Async Algorithms blog post which hinted a values over time using the Continuation* strategy. I wanted to further know how to implemented and experiences its effects. I was not able to find existing examples with this implementation structure that resembles to most common use cases. So I wrote it myself and here its 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 to put together.


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 in order to loop through, I'm setting the visits: AsyncThrowingStream to the callback, the callback is storing the continuation previously declare 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)
 }