Skip to main content
Kelvas blog
  1. Posts/

Convert a closure or a delegate to async / await

·1331 words·7 mins

With Swift 5.5 and the SE-0296 it is now possible to use async / await as in many languages such as C#, Typescript, Javascript or even Rust.

Until now we handled asynchronism in three different ways: the delegated as Apple does a lot, closures (also called completionHandler) as many do or with RxSwift or other libraries like Promise.

But with the arrival of async / await it is no longer necessary to go through all that. So how to convert existing code, especially libraries, to async / await without rewriting everything?

In this article we will see together how to convert delegate and closure to async / await very easily. Regarding libraries like RxSwift or Promise, I let you refer to their different documentations since each of them will have a different implementation.

The functioning of a delegate #

If you are already familiar with this concept, I invite you to go to the next chapter. If you are not, or if you want to review a bit, please stay here.

The delegate is a pattern heavily used by Apple in the core APIs of UIKit. Of course you will find it in many other languages / technologies.

The principle is quite simple: an object A performing a processing will notify an object B inheriting from a protocol C of the end of an action or the need to provide information.

If we want to express it in UML format, we would obtain this:

%%{init: {'theme': 'dark', 'themeCSS': 'svg {background-color: black}'}}%% classDiagram ObjectA..>ObjectB ObjectB--|>ProtocolC

The natural example you think of when you’ve done UIKit is the UITableViewDelegate that you’ve probably implemented dozens of times before:

class MyViewController: UIViewController, UITableViewDelegate {
  
    ...
    
    func tableView(UITableView, viewForHeaderInSection: Int) -> UIView? {
        ...
    }
    
    ...
  
}

In this case, each time the tableView needs the view of a section, it will ask the delegate for this information. Here we are not talking about asynchronism via the delegate at all but only about delegating a decision making.

We have the case of asynchronism with the UIImagePickerControllerDelegate which asks for the implementation of the didFinishPickingMediaWithInfo method which will be called when the user has made his selection, the latter may arrive any time in the future.

The case of closures #

The closure allows to define code with or without parameters that will be executed later. For example when the user login is successful:

func login(username: String, password: String, closure: @escaping (_ error: Error?) -> Void) {
    MyServer.connect(with: username, password: password) { error in
        closure(error)
    }
}

@IBAction func loginButtonPressed() {
    let username = "MyUsername"
    let password = "MyPassword"
    self.login(username: username, password: password) { [weak self] error in
        self?.loader.dismiss()
        if let error = error { 
            self?.displayError(error)
        } else {
            self?.navigateToMenu()
        }  
    }
}

In this case the closure is used to perform a UI processing when the login is completed successfully or not. The login is dependent on an API request that can take several seconds to transit, so we are in the case of asynchronism.

If you do not know the usefulness of [weak self] I invite you to read the present article here.

If you want more information about the keyword @escaping, I invite you to read the present article here.

The tools provided by Apple #

Of course Apple has thought of everything and provided us with tools to convert a delegate or a closure to the async / await pattern. So be careful, when I talk about tools, I’m not talking about an application that will take care of the code for you but a new API:

withCheckedContinuation and withCheckedThrowingContinuation methods to accomplish this mission:

func withCheckedContinuation<T>(
    function: String = #function,
    _ body: (CheckedContinuation<T, Never>) -> Void
) async -> T
func withCheckedThrowingContinuation<T>(
    function: String = #function,
    _ body: (CheckedContinuation<T, Error>) -> Void
) async throws -> T

By analyzing the previous statements you will have understood that the first one does not handle errors while the second one does. Let’s see now how to use them.

Proof by example #

With a closure #

The first advice I can give you is not to replace the existing method with closure by a unique method with async / await but to create an extension.

Let’s go back to our example of the user connection:

class Authenticator {
  
    func login(username: String, password: String, closure: @escaping (_ error: Error?) -> Void) {
        MyServer.connect(with: username, password: password) { error in
            closure(error)
        }
    }
  
}
extension Authenticator {
    
    func loginAsync(username: String, password: String) async throws {
        try await withCheckedThrowingContinuation({ (continuation: CheckedContinuation<Void, Error>) -> Void in
            login(username: username, password: password) { error in
                if let error = error {
                    continuation.resume(throwing: error)
                } else {
                    continuation.resume()
                }
            }
        })
    }
    
}

This way you can use both. In the case of async / await it will be possible to use it in the following way:

do {
  try await Authenticator().loginAsync(username: "myUsername", password: "myPassword")
} catch {
  print("An error occurred: \(error)")
}

It is much more elegant and above all you have now the possibility to use one shape or the other according to your projects and your desire!

With a delegate #

In the case of the delegate it is a little more complicated. It is necessary to encapsulate the delegate in a class and especially to manage the Continuation object well because according to the documentation, this one can be used only once, so it is necessary to make sure that this one is not called 2 times (or more) and is well renewed each time:

You must call a resume method exactly once on every execution path throughout the program.

For the next example I will take the Bluetooth LE device scan which uses the CBCentralManagerDelegate delegate to notify when a new device is found.

It is necessary to add Bluetooth capability to your project for it to work and for the user to access the permission.

As a reminder, our goal is simple: to allow Bluetooth LE device scanning from a simple asynchronous method and therefore without implementing the delegate and the discovery management in our view / controller.

import Foundation
import CoreBluetooth

struct BluetoothLEDevice {

    let identifier: String
    let name: String

    init(peripheral: CBPeripheral) {
        self.identifier = peripheral.identifier.uuidString
        self.name = peripheral.name ?? ""
    }

}

actor BluetoothLEScanWrapper {

    enum BluetoothLEScanError: Error {
        case bluetoothNotAvailable
    }

    private let bluetoothLEDelegate: BluetoothLEDelegate = BluetoothLEDelegate()
    private var activeTask: Task<[BluetoothLEDevice], Error>?

    func scan(for seconds: Double = 3.0) async throws -> [BluetoothLEDevice] {

        if let existingTask = activeTask {
            return try await existingTask.value
        }

        let task = Task<[BluetoothLEDevice], Error> {
            guard bluetoothLEDelegate.bluetoothIsOn else {
                activeTask = nil
                throw BluetoothLEScanError.bluetoothNotAvailable
            }

            self.bluetoothLEDelegate.central.scanForPeripherals(withServices: nil)

            try await Task.sleep(nanoseconds: (UInt64(seconds) * 1_000_000_000))

            let devices = bluetoothLEDelegate.foundPeripheral.compactMap { BluetoothLEDevice(peripheral: $0) }

            bluetoothLEDelegate.central.stopScan()
            bluetoothLEDelegate.foundPeripheral = []

            activeTask = nil

            return devices
        }

        activeTask = task

        return try await task.value

    }

    private final class BluetoothLEDelegate: NSObject, CBCentralManagerDelegate {

        let central: CBCentralManager = CBCentralManager()

        var bluetoothIsOn: Bool = false
        var foundPeripheral: [CBPeripheral] = []

        override init() {
            super.init()
            self.central.delegate = self
        }

        func centralManagerDidUpdateState(_ central: CBCentralManager) {
            self.bluetoothIsOn = central.state == .poweredOn
        }

        func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
            guard !foundPeripheral.contains(where: { $0.identifier == peripheral.identifier }) else { return }

            self.foundPeripheral.append(peripheral)
        }

    }

}

We can now call in an asynchronous method our scan method which completely abstracts our delegate :

do {
    let devices = try await wrapper.scan()
    print("Devices: <\(devices.count)>")
} catch {
    print("An error occured: \(error)")
}

Admit that here too it’s much sexier!

You have probably noticed that the word class has been changed by the word actor. If you want to know more I advise you to stay around, a new article will come soon to explain the difference between a class and an actor.

I hope you enjoyed this article. If you want to know more, feel free to check out the rest of the blog or follow me on Twitter or on Reddit.

See you soon I hope!

Sources #