Swift: convert a delegate to async
February 4, 2024
February 4, 2024
Let’s say we’re using
AVFoundation
to do a screen capture in Swift. We want to response to
didStartRecordingTo and didFinishRecordingTo “events”, which is done
through the use of a delegate such as:
class RecordingDelegate: NSObject, AVCaptureFileOutputRecordingDelegate {
  func fileOutput(_: AVCaptureFileOutput, didStartRecordingTo _: URL, from _: [AVCaptureConnection])
  {
    // Stuff
  }
  func fileOutput(
    _: AVCaptureFileOutput, didFinishRecordingTo _: URL, from _: [AVCaptureConnection],
    error: Error?
  ) {
    // Stuff
  }
}
let session = AVCaptureSession()
let input = AVCaptureScreenInput()
let output = AVCaptureMovieFileOutput()
let delegate = RecordingDelegate()
session.addInput(input)
session.addOutput(output)
session.startRunning()
output.startRecording(to: URL(filePath: "test.mov"), recordingDelegate: delegate)
That’s all good but the delegate makes my life a bit harder in terms of managing the control flow of the code.
I would much rather something using async/await, e.g. (hypothetical code):
let events = output.startRecording(to: URL(filePath: "test.mov"))
await events.didStartRecording
// Do stuff now the recording has started
output.stopRecording()
await events.didFinishRecording
// Do stuff now the recording is finished
In order to achieve that, we need a bit of plumbing code. Let’s add callbacks to our delegate:
class RecordingDelegate: NSObject, AVCaptureFileOutputRecordingDelegate {
  var didStartRecording: () -> Void = {}
  var didFinishRecording: (_ error: Error?) -> Void = { _ in }
  func fileOutput(_: AVCaptureFileOutput, didStartRecordingTo _: URL, from _: [AVCaptureConnection])
  {
    self.didStartRecording()
  }
  func fileOutput(
    _: AVCaptureFileOutput, didFinishRecordingTo _: URL, from _: [AVCaptureConnection],
    error: Error?
  ) {
    self.didFinishRecording(error)
  }
}
From there, we can use the withCheckedContinuation
function to convert a callback to an async result:
let delegate = RecordingDelegate()
async let didStartRecording: () = withCheckedContinuation { continuation in
  delegate.didStartRecording = {
    continuation.resume()
  }
}
async let didFinishRecording: () = withCheckedContinuation { continuation in
  delegate.didFinishRecording = { error in
    if let error = error {
      continuation.resume(throwing: error)
    } else {
      continuation.resume()
    }
  }
}
output.startRecording(to: URL(filePath: "test.mov"), recordingDelegate: delegate)
await didStartRecording
// Do stuff now the recording has started
output.stopRecording()
await didFinishRecording
// Do stuff now the recording is finished
Thanks to withCheckedContinuation, we can get an async result for the
didStartRecording and didFinishRecording events, that we’re free to
await whenever is most convenient!
Note: the code above is not perfect. In some cases, we may get an
event on didFinishRecordingTo with an error, before
didStartRecordingTo was called at all. In that case, that example
would just hang forever.