Async Programming in Swift with AsyncTask
On iOS and MacOS, the main thread renders UI and handles events. A lot of the tasks that our apps perform, including data deserialization and image processing, take a while to finish. When we run them on the main thread, our app stops responding to user interaction and become unresponsive. To ensure a good user experience, we need to keep expensive tasks off the main thread.
The best way to achieve this is to write asynchronous code. Apple provides many abstractions for asynchronous programming among which Grand Central Dispatch (GCD)/libdispatch is the most popular one. If you are not familiar with GCD, here is an excellent introduction.
GCD is well designed and easy to use. It introduces a number of high level abstractions that make our life easy. Dispatch queue, for example, is a high level abstraction of thread that takes a block of code and execute it asynchronously. To run a task on a the background thread, just submit a block to the appropriate dispatch queue.
Blocks are simple and intuitive, but they do not scale with complexity.
As the task becomes more complicated, we often end up in a callback hell, writing deeply nested blocks that are difficult to understand.
This problem is common to many programming languages. In fact, the term callback hell was coined by Javascript developers, who suffered deeply from it because of the language’s single-threaded nature. Fortunately, developers in the Javascript community stood up against this difficulty and invented many libraries, including async.js, promise, and co.js, to model asynchronous programs; some of them, such as promise and async function, worked so well that they became a part of the Javascript standard.
Likewise, the Swift community has come up with great ideas about asynchronous programming in Swift. Some people created syntactic sugar for GCD, such as Async; some implemented Future and Promise, such as Bolt, PromiseKit, FutureKit, and BrightFutures; some embraced Functional Reactive Programming, such as ReactiveCocoa and RxSwift.
I believe that async function is the future for asynchronous programming in Swift. So I created AsyncTask, inspired by the async function in C#, Scala, Dart, and ES7.
Here is how AsyncTask compares with others:
Promise makes the GCD code more readable, but it requires extra syntax and workarounds for the scopes introduced by new functions. AsyncTask, on the other hand, feels more familiar because the code resembles the structure of a synchronous function.
In AsyncTask, a Task represents the eventual result of an asynchronous operation, as do Future and Promise in other libraries. It can wrap both synchronous and asynchronous APIs. To create a Task, initialize it with a closure. To make it reusable, write functions that return a task.
To get the result of a Task, use async or await. async is just like dispatch_async, and you can supply a completion handler. await, on the contrary, blocks the current thread and waits for the task to finish.
A Task is much more than a Future or Promise.
- It is composable, allowing you to build complex workflow.
- It supports native error handling with do-catch and try.
- It is protocol oriented; so you can turn any object into a Task.
Composing Tasks
You can use multiple await expressions to ensure that each statement completes before executing the next statement:
You can also call awaitFirst and awaitAll on a collection of tasks to execute them in parallel:
Handling Errors
Swift provide first-class support for error handling. In AsyncTask, a ThrowableTask takes a throwing closure and propagates the error.
Extending Tasks
AsyncTask is protocol oriented; it defines TaskType and ThrowableTaskType and provides the default implementation of async and await using protocol extension. In other words, these protocols are easy to implement, and you can await on any object that confronts to them. Being able to extend tasks powerful because it allows tasks to encapsulate states and behaviors.
In the following example, by extending NSURL to be TaskType, we make data fetching a part of the NSURL class. To confront to the TaskType protocol, just specify an action and the return type.
This extension allows us to write the following code:
let (data, response) = try! NSURL(string: "www.google.com")!.await()
A Task can represent more complicated activities, even those involving UI. In the following example, we use an ImagePickerTask to launch a UIImagePickerViewController and wait for the user to choose an image. Once the user selects an image or press the cancel button, the view controller dismisses, and the task returns with the selected image.
The ImagePickerTask knows when the user has picked an image or canceled because it is the UIImagePickerViewController’s delegate.