AsyncStream... The `QuakeMonitor` Example

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


I have created a couple of bridge classes on some of the Apple Services/frameworks, like the Authorization Service, that is 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 the 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*, and it will no longer be invoked unless the user removes the iOS application from their device. For this example, I will use withCheckedThrowingContinuation, which is one of the types of Continuation.


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 at values over time using the Continuation* strategy. I wanted to know how to implement and experience its effects further. I was not able to find existing examples with this implementation structure that resembles the most common use cases. So I wrote it myself, and 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 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 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)
 }