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() | |
| } | |
| } | |
| } | |
| } |
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.