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:
Maintain an optional instance of the continuation in your delegate class
Use the continuation inside the delegate methods of the framework either to yield/return value/s or terminate/finish with an error.
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)
}