Bridge Async Await, Delegates, Combine and Completion blocks

Async await introduces a simpler approach to concurrency, so even in ongoing projects, it’s a good idea to incorporate async await syntax. However, instead of adding async await throughout the entire project, it’s better to gradually introduce the syntax in small parts of the code. That’s why it’s very important to bridge code between async await, delegates, Combine, and completion blocks.

For example, maybe we already have a service with a Combine interface, and we also want to introduce an async interface so that whoever is using the service can utilize it using await. However, we don’t implement the service using async; we simply provide an interface by reusing the current implementation from Combine.

Continuation

Before we start examining the different examples of bridging code, let’s take a look at the tool async provides us for this purpose, called Continuation.

A Continuation is created when any await instruction is executed, and what it does is create a suspension point, storing all the state of the program at that moment. This allows us to resume the program in the same state at a later time. The purpose of this is to avoid creating threads. The runtime establishes a suspension point, stores the state, hands over the thread to other tasks, and then, when it’s time to resume the async code, restores the previous state.

This process is handled for us when we use await, but we can also create these continuations ourselves. They can be useful for bridging between Async Await, delegates, Combine and Completion blocks.

CheckedContinuation and UnsafeContinuation

CheckedContinuation and UnsafeContinuation are defined for Apple as mechanisms to interface between synchronous and asynchronous code. Continuations are expected to be resumed once and only once, and the difference between them lies in their behavior:

  • CheckedContinuation: Performs runtime checks. In the case of any mistake where you don’t resume the continuation or you resume the continuation multiple times, a warning will be displayed in the console, but the app will not crash.
  • UnsafeContinuation: Does not perform any checks, making it better for performance than CheckContinuation. However, if you mistakenly resume the continuation more than once, the app will crash.

The choice between them depends on your specific requirements, but unless performance is crucial in a particular part of the app, the checked version is often preferred. Each version also has variations with throwing and non-throwing errors. Now let’s explore the syntax; although the syntax for both is very similar, we’ll focus on the checked version in this article.

Let’s examine the syntax in an async throwing function that should return a String. We receive the continuation in the closure, and we should resume the continuation only once. There are three ways to resume the continuation: by throwing an error, returning a result, or returning the expected value.

Swift
func syntaxContinuation() async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        continuation.resume(with: Result<String, Error>)
        continuation.resume(throwing: Error)
        continuation.resume(returning: String)
    }
}

From Completion blocks to Async Await

If we already have the following interface using completion blocks that return a result, we can provide an extension for async code using a CheckedContinuation. The extension utilizes the completion block interface and resumes the continuation with the result.

Swift
protocol ContactsAPI {
    func fetchContacts(_ completion: @escaping (Result<[Contact], Error>) -> Void)
}

extension ContactsAPI {
    func fetchContacts() async -> Result<[Contact], Error> {
        return await withCheckedContinuation { continuation in
            fetchContacts { result in
                continuation.resume(returning: result)
            }
        }
    }
}

If we aim to provide a throwing function instead of the result, we can use withCheckedThrowingContinuation and resume the continuation with the value or throw the error.

Swift
extension ContactsAPI {
    func fetchContacts() async throws -> [Contact] {
        return try await withCheckedThrowingContinuation { continuation in
            fetchContacts { result in
                switch result {
                case .success(let contacts):
                    continuation.resume(returning: contacts)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        }
    }
}

From Async Await to Completion block

If we already have an interface using async, we can also provide an interface using completion blocks. We reuse the current async implementation, then call the completion block with the result. It’s essential to create a Task because we are in a synchronous context.

Swift
protocol ContactsAPI {
    func fetchContacts() async -> Result<[Contact], Error>
}

extension ContactsAPI {
    func fetchContacts(_ completion: @escaping (Result<[Contact], Error>) -> Void) {
        Task {
            completion(await fetchContacts())
        }
    }
}

From Delegates to Async Await

Bridging delegates to async code is one of the most common use cases, as it’s still prevalent to use delegates with certain frameworks like CoreLocation. In this example, let’s explore how we can create a service that returns the user location using async await.

We have a LocationService class with all the setup for requesting the user location. When calling requestLocation, we return withCheckedThrowingContinuation because we will receive the user location in a delegate call. Therefore, we need to store the continuation in a property. In case the user denies the location, we simply resume the continuation by returning an error, and we don’t store the continuation in this scenario.

Swift
import CoreLocation

enum LocationError: Error {
    case denied
}

final class LocationService: NSObject, CLLocationManagerDelegate {
    private let manager: CLLocationManager
    private var locationContinuation: CheckedContinuation<CLLocation, Error>?
    
    override init() {
        self.manager = CLLocationManager()
        super.init()
    }
    
    func requestLocation() async throws -> CLLocation {
        return try await withCheckedThrowingContinuation { [weak self] continuation in
            guard let self else { return continuation.resume(throwing: CancellationError() )}
            
            manager.delegate = self
            switch manager.authorizationStatus {
            case .authorizedAlways, .authorizedWhenInUse:
                locationContinuation = continuation
                manager.startUpdatingLocation()
            case .notDetermined:
                locationContinuation = continuation
                manager.requestWhenInUseAuthorization()
            default:
                continuation.resume(throwing: LocationError.denied)
            }
        }
    }
...

When we receive user permissions, if they are authorized, we request the user location; otherwise, we resume the continuation with an error. Upon receiving the user location or an error, we resume the continuation with the location or return an error. Continuations are expected to be resumed once and only once, so it’s crucial to release the continuation from the property once it’s resumed. This prevent to resume the continuation multiple times.

Swift
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
    switch manager.authorizationStatus {
    case .notDetermined:
        break
    case .authorizedAlways, .authorizedWhenInUse:
        manager.startUpdatingLocation()
    default:
        locationContinuation?.resume(throwing: LocationError.denied)
        locationContinuation = nil
    }
}

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let location = locations.first {
        locationContinuation?.resume(returning: location)
        locationContinuation = nil
    }
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    locationContinuation?.resume(throwing: error)
    locationContinuation = nil
}

From Combine to Async Await

When we aim to provide an interface from Combine to Async Await, we need to think that Combine produces a stream of values that can be publish multiple times, instead of just one value. We can use the values extension from a Publisher, which converts a publisher into an async sequence. Iterating over an async sequence is done with an await for loop. For more details about async sequences, take a look about async sequences in my previous post.

Swift
import Combine

protocol ContactsAPI {
    func fetchContacts() -> AnyPublisher<[Contact], Error>
}

class ContactsHandler {
    let api: ContactsAPI
    
    init(api: ContactsAPI) {
        self.api = api
    }
    
    func listenToMultiplePusblishedElements() async throws {
        for try await listContacts in api.fetchContacts().values {
            // Perform logic
        }
    }
}

If you expect the publisher to return a single value, you can use the iterator from the sequence for that purpose.

Swift
func getFirstListofContacts() async throws -> [Contact] {
    var iterator = api.fetchContacts().values.makeAsyncIterator()
    return try await iterator.next() ?? []
}

From Async Await to Combine

If we already have an interface using async and we want to provide a Combine interface, we can use Deferred and Future from Combine to return the publisher. This involves creating an asynchronous context to use the async interface and resuming the promise with the result. A Future promise is expected to be resumed only once, so this publisher will emit one value and then finish.

Swift
import Combine

protocol ContactsAPI {
    func fetchContacts() async throws -> [Contact]
}

extension ContactsAPI {
    func fetchContacts() -> AnyPublisher<[Contact], Error> {
        return Deferred {
            return Future<[Contact], Error> { promise in
                Task {
                    do {
                        let contacts = try await fetchContacts()
                        promise(.success(contacts))
                    } catch {
                        promise(.failure(error))
                    }
                }
            }
        }
        .eraseToAnyPublisher()
    }
}

From Async Sequence to Combine

If we are using an async sequence, the previous approach cannot be used because the Future promise can only be resumed once. To handle this scenario, we can create the following extension to convert an async sequence, whether it’s throwing an error (AsyncThrowingStream) or not (AsyncStream). In both cases, we iterate over the async sequence and send the value to the returned publisher.

Swift
import Combine

extension AsyncThrowingStream where Failure == Error {
    func toPublisher() -> AnyPublisher<Element, Failure> {
        let subject = PassthroughSubject<Element, Failure>()
        
        Task {
            do {
                for try await value in self {
                    subject.send(value)
                }
            } catch {
                subject.send(completion: .failure(error))
            }
        }
        
        return subject.eraseToAnyPublisher()
    }
}

extension AsyncStream {
    func toPublisher() -> AnyPublisher<Element, Never> {
        let subject = PassthroughSubject<Element, Never>()
        
        Task {
            for await value in self {
                subject.send(value)
            }
        }
        
        return subject.eraseToAnyPublisher()
    }
}

And then we can use the publisher like any other publisher:

Swift
AsyncThrowingStream<Contact, Error> {
    try await Task.sleep(for: .seconds(1))
    return "some Contact"
}
    .toPublisher()
    .sink(receiveCompletion: { completion in
        // Handle completion or error
    }, receiveValue: { _ in
        ...
    })
    .store(in: &cancellables)

Conclusion

We explored a set of tools that can be used to bridge Async Await, Combine, Delegates, and Completion blocks. This approach allows for incremental implementation of Async Await, taking small steps instead of having to re-implement everything using async. I’ve provided simple examples to keep this article as a reference, as it addresses a very common task when working on a project.