Swift: convert a delegate to async

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.

Want to leave a comment?

Join the discussion on Twitter or send me an email! đź’Ś
This post helped you? Buy me a coffee! 🍻