How turn my function into async pattern

Suppose I have the following function:

func doWork(_ someValue: Int, completionHandler: () -> Void) {
    let q = DispatchQueue()
    q.async {
        // Long time of work
        completionHandler()
    }
}

How do I turn it into async function so that I can call it using await doWork()? Are there guidelines/principles/practices for this purpose?

Accepted Reply

I assume you cannot modify the API entirely? If you have full control, you can simply do something like:

func doWork(_ someValue) async {
	// Long time of work
}

If you still want to maintain the completion handler API, but wrap it with an async version, you should take a look at Continuations. There's an article on hackingwithswift.com that might help: https://www.hackingwithswift.com/quick-start/concurrency/how-to-use-continuations-to-convert-completion-handlers-into-async-functions.

Using this approach, your existing function could be wrapped by a new async version quite easily.

func doWork(_ someValue: Int, completionHandler: @escaping () -> Void) {
	let q = DispatchQueue(label: "MyLabel")
	q.async {
		// Long time of work
		completionHandler()
	}
}

func doWork(_ someValue: Int) async {
	await withCheckedContinuation { continuation in
		// Call the existing completion handler API.
		doWork(someValue) {
			// Resume the continuation to exit the async function.
			continuation.resume()
		}
	}
}

Care should be taken to ensure you always call the completion handler in the original API, otherwise you could have a situation where your continuation doesn't resume. You know you've usually done this if you see something like this in the console: SWIFT TASK CONTINUATION MISUSE: doWork(_:) leaked its continuation!.

Continuations can return values, and even throw errors if needed.

If you're curious about other functions, you can often use Xcode to refactor the function to be asynchronous, or generate an async wrapper (although mileage may vary depending on the function complexity). You can do this by selecting the function name, right clicking and choosing "Refactor", and then picking either "Convert Function to Async" or "Add Async Wrapper". The code generated from "Add Async Wrapper" is as follows, which isn't far from the example above:

@available(*, renamed: "doWork(_:)")
func doWork(_ someValue: Int, completionHandler: @escaping () -> Void) {
	let q = DispatchQueue(label: "MyLabel")
	q.async {
		// Long time of work
		completionHandler()
	}
}

func doWork(_ someValue: Int) async {
	return await withCheckedContinuation { continuation in
		doWork(someValue) {
			continuation.resume(returning: ())
		}
	}
}

Hope that helps.

-Matt

Replies

I assume you cannot modify the API entirely? If you have full control, you can simply do something like:

func doWork(_ someValue) async {
	// Long time of work
}

If you still want to maintain the completion handler API, but wrap it with an async version, you should take a look at Continuations. There's an article on hackingwithswift.com that might help: https://www.hackingwithswift.com/quick-start/concurrency/how-to-use-continuations-to-convert-completion-handlers-into-async-functions.

Using this approach, your existing function could be wrapped by a new async version quite easily.

func doWork(_ someValue: Int, completionHandler: @escaping () -> Void) {
	let q = DispatchQueue(label: "MyLabel")
	q.async {
		// Long time of work
		completionHandler()
	}
}

func doWork(_ someValue: Int) async {
	await withCheckedContinuation { continuation in
		// Call the existing completion handler API.
		doWork(someValue) {
			// Resume the continuation to exit the async function.
			continuation.resume()
		}
	}
}

Care should be taken to ensure you always call the completion handler in the original API, otherwise you could have a situation where your continuation doesn't resume. You know you've usually done this if you see something like this in the console: SWIFT TASK CONTINUATION MISUSE: doWork(_:) leaked its continuation!.

Continuations can return values, and even throw errors if needed.

If you're curious about other functions, you can often use Xcode to refactor the function to be asynchronous, or generate an async wrapper (although mileage may vary depending on the function complexity). You can do this by selecting the function name, right clicking and choosing "Refactor", and then picking either "Convert Function to Async" or "Add Async Wrapper". The code generated from "Add Async Wrapper" is as follows, which isn't far from the example above:

@available(*, renamed: "doWork(_:)")
func doWork(_ someValue: Int, completionHandler: @escaping () -> Void) {
	let q = DispatchQueue(label: "MyLabel")
	q.async {
		// Long time of work
		completionHandler()
	}
}

func doWork(_ someValue: Int) async {
	return await withCheckedContinuation { continuation in
		doWork(someValue) {
			continuation.resume(returning: ())
		}
	}
}

Hope that helps.

-Matt

+1 on Matt Cox’s answer.

The only thing I’d add is that you should try to support cancellation. Cancellation is central to structure concurrency, which is the model that Swift concurrency is trying to promote.

If your underlying completion handler API supports cancellation, wire that up. If not, you might consider whether it’s worthwhile implementing ‘cancellation by abandonment’, that is, allow the Swift async function to return before the completion handler is called and have the completion handle do nothing in that case.

Getting cancellation right can be quick tricky. Your entry point should be the withTaskCancellationHandler(handler:operation:) function. Post back here if you have questions about it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"