Categories
Swift

Wrapping async-await with a completion handler in Swift

It is not often when we need to wrap an async function with a completion handler. Typically, the reverse is what happens. This need can happen in codebases where the public interface can’t change just right now, but internally it is moving towards async-await functions. Let’s jump in and see how to wrap an async function, an async throwing function and an async throwing function what returns a value.

To illustrate how to use it, we’ll see an example of how a PhotoEffectApplier type has a public interface consisting of completion handler based functions and how it internally uses PhotoProcessor type what only has async functions. The end result looks like this:

struct PhotoProcessor {
func process(_ photo: Photo) async throws -> Photo {
// …
return Photo(name: UUID().uuidString)
}
func setConfiguration(_ configuration: Configuration) async throws {
// …
}
func cancel() async {
// …
}
}
public final class PhotoEffectApplier {
private let processor = PhotoProcessor()
public func apply(effect: PhotoEffect, to photo: Photo, completion: @escaping (Result<Photo, Error>) -> Void) {
Task(operation: { try await self.processor.process(photo) }, completion: completion)
}
public func setConfiguration(_ configuration: Configuration, completion: @escaping (Error?) -> Void) {
Task(operation: { try await self.processor.setConfiguration(configuration) }, completion: completion)
}
public func cancel(completion: @escaping (Error?) -> Void) {
Task(operation: { await self.processor.cancel() }, completion: completion)
}
}

In this example, we have all the interested function types covered: async, async throwing and async throwing with a return type. Great, but let’s have a look at these Task initializers what make this happen. The core idea is to create a Task, run an operation, and then make a completion handler callback. Since most of the time we need to run the completion on the main thread, then we have a queue argument with the default queue set to the main thread.

extension Task {
@discardableResult
init<T>(
priority: TaskPriority? = nil,
operation: @escaping () async throws -> T,
queue: DispatchQueue = .main,
completion: @escaping (Result<T, Failure>) -> Void
) where Success == Void, Failure == any Error {
self.init(priority: priority) {
do {
let value = try await operation()
queue.async {
completion(.success(value))
}
} catch {
queue.async {
completion(.failure(error))
}
}
}
}
}
extension Task {
@discardableResult
init(
priority: TaskPriority? = nil,
operation: @escaping () async throws -> Void,
queue: DispatchQueue = .main,
completion: @escaping (Error?) -> Void
) where Success == Void, Failure == any Error {
self.init(priority: priority) {
do {
try await operation()
queue.async {
completion(nil)
}
} catch {
queue.async {
completion(error)
}
}
}
}
}
extension Task {
@discardableResult
init(
priority: TaskPriority? = nil,
operation: @escaping () async -> Void,
queue: DispatchQueue = .main,
completion: @escaping () -> Void
) where Success == Void, Failure == Never {
self.init(priority: priority) {
await operation()
queue.async {
completion()
}
}
}
}
view raw Async.swift hosted with ❤ by GitHub

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.