I am reading about actors and as a teaching example I am rewriting an old piece of code which used delegation to use async await. I am using checked continuations to interface the sync to async.
I read this introduction: https://www.swiftbysundell.com/articles/swift-actors/ As I understand, since the actors are re-entrant we need to keep track of the work that's currently being performed - i.e. the active task.
What I am not sure about is how this marries together with continuations.
Here's my code. Thanks for the help!
public actor MyNewDelegateSub {
private var activeTask: Task<BLEDevice, Error>?
private var checkedThrowingContinuation: CheckedContinuation<BLEDevice, Error>?
private var bleDelegate: BluetoothLEDelegate
public func returnFunctionThatReplacesDelegateCallbacks() async throws -> BLEDevice? {
if let existingTask = activeTask {
return try await existingTask.value
}
let task = Task<BLEDevice?, Error> {
return try await withCheckedThrowingContinuation({ [weak self] (continuation: CheckedContinuation<BLEDevice, Error>) in
bleDelegate.checkedThrowingContinuation = continuation
do{
bleDelegate.scan()
activeTask = nil
} catch {
activeTask = nil
throw error
})
}
activeTask = task
return try await task.value
}
}
private final class BluetoothLEDelegate: NSObject, CBCentralManagerDelegate {
private let centralManager = CBCentralManager()
var checkedThrowingContinuation: CheckedContinuation<BLEDevice, Error>?
init() {
centralManager.delegate = self
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
checkedThrowingContinuation?.resume(returning: BLEDevice(peripheral) )
checkedThrowingContinuation = nil
}
....
}
Yes, it is perfectly acceptable to use withCheckedThrowingContinuation
in a Task
.
That having been said, there are a bunch of issues with this code snippet, though:
In your do
-try
-catch
, there is no try
. Thus there is nothing to ever catch
. Also, you wouldn’t throw
from inside a withCheckedThrowingContinuation
, anyway, you would resume
with the error.
Whenever you use unstructured concurrency, such as Task {…}
, you are responsible for writing your own cancelation handling code, e.g. withTaskCancellationHandler
.
In a more stylistic observation, but rather than passing the continuation to the bluetooth manager object (which entangles these two types together, introduces non-local reasoning about the continuation which practically invites its eventual misuse, etc.), I might advise that you decouple these two types:
give the bluetooth manager object closure properties associated with the relevant delegate methods, and have the delegate methods call those closures … now the bluetooth manager is no longer tightly-coupled with the continuation code; and
inside the withCheckedThrowingContinuation
, spin up the bluetooth manager, set its closures, and inside those closures, resume
and cleanup as needed.
The right pattern for discover of bluetooth devices is probably an AsyncSequence
, e.g., an AsyncStream
, anyway, not a task. See WWDC 2021 video Meet AsyncSequence.