From callbacks to async / await in Swift

Introduction

It's been a while since I wrote a blog post, (actually, this is my first, I have no excuses!), but I really wanted to touch upon asynchronous programming in Swift. Most of the code you write in Swift must at least take asynchronicity into consideration at some level. When updating the ui after some computation that typically does not belong in the controller layer, even then you have to think about where the correct place for this piece of code should be.

Updating UI on a thread other than the main thread is a common mistake that can result in missed UI updates, visual defects, data corruptions, and crashes.

When beginners start out with iOS, they use callbacks to execute certain functionality when another process has finished. Usually the final part of the chain of functionality which is executed upon operation completion, if required, would be to update the UI. Assuming this were the case, using callbacks we would have to pass an 'Update the UI' callback all the way through the chain of callbacks repeatedly until we get to the end of the chain, finally calling that callback to update the UI, assuming we don't want to pollute our domain / infrastructure logic with a call to updating the UI because we value Separation of Concerns.

Let's take a look at how a piece of code can evolve from simple callbacks, through to utilising async / await, allowing you to write completely blocking code in a background thread with only a few extra lines of boilerplate each time. This is very close to the mayhem that I wrote when I first started out working with Swift.

Callbacks

run_from_callbacks-1

Our use case: we're building an application that first will make a HTTP request to retrieve some json. Within this json is the URL of an image that we want to download. Finally, once we have downloaded that image, we should present it to the user and display it in the UI. Let's build this with callbacks first.

Step 1 - The initial HTTP Request

The first step would be to download some json using Data(contentsOf:), which just performs a GET request to an endpoint without any headers or additional information. In fact, this is the worst way to perform a download; Data(contentsOf:) should be used for local files only, but for the purposes of this blog post, assume no error checking is absolutely deliberate.

class DefaultViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        let response = try! Data(contentsOf: URL(string: "http://our-data-endpoint.com")!)
        
        let jsonData = try! JSONSerialization.jsonObject(with: response, options: []) as! [String: Any]
        
        let imageUrl = jsonData["image_url"]
    }
}

In this intentionally simplistic example I've removed libraries that we might use, we're using Data(contentsOf:) which you wouldn't do except with anything locally, no error checking is present, guard statements aren't used etc. Force unwrap all the things!!

The first thing we do is synchronously (note: we'll make it async next) download a json string into the response variable, convert that json into an object over which we can iterate in jsonData, and then grab the image_url key from jsonData; so imageUrl now contains the url of the image we want to download.

The problem with this right now is that we're blocking the main thread (and run loop) while the json is downloading. The UI is in the main thread, so if the user is trying to do anything else like dragging or navigating somewhere else at the same time, well it won't work - these actions will be queued to be executed after the download has finished.

What can we do about this? We can use a background thread!

Step 2 - Multi-threading

multithreading-1

Grand Central Dispatch, or GCD for short, provides a simple and powerful API through which we can perform 'background' operations in a separate thread.

DispatchQueue.global(qos: .background).async {
    let response = try! Data(contentsOf: URL(string: "http://our-data-endpoint.com")!)
}

The above extra line of code (the first line), assuming we don't count the closing parenthesis at the end, places any calls within those parenthesis onto a background thread. Now we're not blocking the main thread so any UI changes like animations or user-invoked events like button presses can continue to be handled.

The problem is soon that we're going to be violating the principle of single responsibility and this class is quickly going to increase in size. If we place the image download code in here too, then this not only becomes a nightmare to test in the future but harder to refactor too.

Let's write code more towards the 'right way' and split things up a bit.

Step 3 - SRP

Let's place our JsonDownloader into a struct and add an onComplete handler (closure) as a second parameter to be executed when the work has been completed asynchronously.

Why do we have to do this? Well, if we're using .async { } within a synchronous function, we can't just do a return from the async call because the async call returns immediately allowing the runloop to continue handling other things. One cannot simply return a variable from an asynchronous context to a synchronous one, because the asynchronous one returns immediately and the schronous code just continues like nothing happened. So we have to execute the callback function that we can pass in at the end instead.

Note we could still just hardcode the contents of each callback together but then we would end up with callback hell as well, and that's without the cleaner separation being shown here

struct JsonDownloader
{
    public func downloadContentsOfUrl(url: URL, onComplete: @escaping (_ url: URL) -> UIImage) -> Void {
        // This async call will be thrown onto a thread and then this function will return immediately
        DispatchQueue.global(qos: .background).async {
            let json = try! Data(contentsOf: url)
            
            let jsonData = try! JSONSerialization.jsonObject(with: json, options: []) as! [String: Any]

            onComplete(URL(string: jsonData["image_url"] as! String)!)
        }
    }
}

Notice that we've defined the onComplete callback to be a closure that takes a url and must return a UIImage. This is the next step in our process. We need to create this closure in our UIViewController and then pass it into our JsonDownloader to be executed upon completion of the json download at a later time.

Step 4 - Closures, Closures, Closures

First, let's create our closure that takes a String and returns a UIImage. Then we can pass it into our call to JsonDownloader.

class ViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        let imageDownloadCallback: (URL) -> UIImage = { url in
            return UIImage(data: Data(contentsOf: url))!
        }
        
        let jsonDownloader: JsonDownloader = JsonDownloader()
        
        jsonDownloader.downloadContentsOfUrl(url: URL(string: "ourUrl.com")!, onComplete: imageDownloadCallback)
        
        print("This line will likely be printed before the asynchronous call in JsonDownloader finishes")
    }
}

Remember the call to downloadContentsOfUrl(url:) has an asynchronous call in a background thread being executed inside of it, so the print() at the bottom will execute first and then later on our callback will be executed to retrieve the image.

Finally, we need to display this image in the UI. We should absolutely not be updating our UI from anywhere within the business logic of the application, never mind the infrastructure area that makes API calls. It's going to become an unorganised mess in a larger codebase if we do this - UI changes should be made in the view controller, which facilitiates messages between the view and the model.

If we want to try writing our UI updating code in our view controller, but it must be executed at the end of an asynchronous call in some other file in the model layer somewhere else, then what can we do about that? Enter... you guessed it... another callback. This time it must take the instance of UIImage that we have downloaded and display this in the UI.

So now we're passing callbacks within callbacks, and we have to update the JsonDownloader to take both callbacks as well. Take a look at the following awful (but working) code.

struct JsonDownloader
{
    public func downloadContentsOfUrl(
        url: URL,
        onUrlRetrieved: @escaping (_ url: URL, (UIImage) -> Void) -> Void,
        onImageDownloaded: @escaping (_ image: UIImage) -> Void
    ) -> Void {
        // This async call will be thrown onto a thread and then this function will return immediately
        DispatchQueue.global(qos: .background).async {
            let json = try! Data(contentsOf: url)
            
            let jsonData = try! JSONSerialization.jsonObject(with: json, options: []) as! [String: Any]
            
            // Calling our first callback, and passing in the second, violating LoD
            onUrlRetrieved(URL(string: jsonData["image_url"] as! String)!, onImageDownloaded)
        }
    }
}

The first thing to notice here is that downloadContentsOfUrl now takes two callback arguments, one to be executed once the url has been retrieved, and the second one to be executed once the image has been downloaded. This will require passing that uiUpdatingCallback through two functions, just so the second one can utilise it, which violates the Law of Demeter.

class ViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        let uiUpdatingCallback: (UIImage) -> Void = { image in
            // Don't forget, we must do any UI stuff on the main thread
            DispatchQueue.main.async {
                let imageView = UIImageView(image: image)

                imageView.frame = CGRect(x: 0, y: 0, width: 300, height: 300)

                self.view.addSubview(imageView)
            }
        }
        
        let imageDownloadCallback: (URL, (UIImage) -> Void) -> Void = { url, onImageDownloaded in
            let image = UIImage(data: try! Data(contentsOf: url))!

            onImageDownloaded(image)
        }
        
        let jsonDownloader: JsonDownloader = JsonDownloader()

        jsonDownloader.downloadContentsOfUrl(
            url: URL(string: "ourUrl.com")!,
            onUrlRetrieved: imageDownloadCallback,
            onImageDownloaded: uiUpdatingCallback
        )

        print("This line will likely be printed before the asynchronous call in JsonDownloader finishes")
    }
}

Here we are defining our uiUpdatingCallback in the view controller. This will be executed at the end of the process by the imageDownloadCallback which we define next. The imageDownloadCallback will be executed when the JsonDownloader has finished downloading the json data containing the image url asynchronously.

We then pass two callbacks to the downloadContentsOfUrl() function call and this uses the first callback within it, but passes the second callback (the ui updating one) through to be used later once the image has been downloaded asynchronusly.

Can you see how this is starting to become a bit of a nightmare? We just want to be able to define what will happen next in the correct place, synchronously, without having to pass callbacks within callbacks.

Let's rewrite this with async / await.

Async / Await

Let's take a look at the entry point first, our ViewController. There are other things we need to do to make this work involving promises, but for now, this is the top level code.

class ViewController: UIViewController
{
    override func viewDidLoad()
    {
        super.viewDidLoad()
        
        async {
            let jsonDownloader: JsonDownloader = JsonDownloader()
            let imageDownloader: ImageDownloader = ImageDownloader()
        
            let json: [String: AnyObject] = try! await(jsonDownloader.downloadContentsOfUrl("ourUrl.com"))
            
            let image: UIImage = try! await(imageDownloader.downloadImage(json["image_url"]))
            
            DispatchQueue.main.async { 
                let imageView = UIImageView(image: image)
                
                imageView.frame = CGRect(x: 0, y: 0, width: 300, height: 300)
                
                view.addSubview(imageView)
            }
        }
    }
}

We've placed the image downloader callable into a class, but that's not the main change here. Firstly we have async { } which is the syntactically sugared equivalent of dispatching into a background thread. Secondly, we have await(), which will wait for the promise within it to resolve asynchronously. These can be two, separate, asynchronous-using structs and as long as we use promises, they will wait. No callbacks, no passing functions around.

This means that an asynchronous piece of code can return a value, via it's Promise, and the await() call handles the waiting for that value to be returned. We're writing code that looks synchronous!

You may be a little confused about why we don't just do the following directly in the view controller itself:

DispatchQueue.global(qos: .background).async {
    let jsonDownloader: JsonDownloader = JsonDownloader()
    let imageDownloader: ImageDownloader = ImageDownloader()
    
    let json: [String: AnyObject] = jsonDownloader.downloadContentsOfUrl("ourUrl.com")
    
    let image: UIImage = imageDownloader.downloadImage(URL(string: json["image_url"]))
    
    DispatchQueue.main.async {
        // -- SNIP -- Add image to UIImageView and add a subview in UIView
    }
}

Looks great, right? But what if you have the background threading within the JsonDownloader class instead, as many other classes do, both core and third-party, and not within your view controller?

  • The call to JsonDownloader will return immediately
  • The call to ImageDownloader will try and use a url that will actually be nil because downloadContentsOfUrl has not finished yet
  • The application will crash

If you restrict yourself to only using background queues from the view controller, and in many cases you need more than one thread to perform some functionality, you'll be coordinating all this logic from the controller and it's going to become a bit of a nightmare. So much for "fat models and skinny controllers" (or rather "skinny models and skinny controllers" as is the current collective thought process at the moment)!

We need to change our JsonDownloader slightly to work with this though, instead returning Promises from PromiseKit.

struct JsonDownloader
{
    public func downloadContentsOfUrl(_ url: URL) -> Promise<String> {
        return Promise { resolve, reject in
            let jsonData = try! Data(contentsOf: url)

            let jsonData = try! JSONSerialization.jsonObject(with: response, options: []) as! [String: Any]

            return resolve(url: jsonData['image_url'])
        }
    }
}

That's it! Our Promise object now returns a call to resolve which internally is only executed once the download has completed. This enables us to use await() and write more synchronous-looking code without any callbacks.

Conclusion

I've been using both PromiseKit and AwaitKit for several months now and the changes from writing asynchronous code with callbacks to this have been staggering - they've made my development so much easier and more fun!

I hope this post convinces some to take a look at async / await and change from callback hell to something that looks a lot more clean and easy to work with.

Let me know in the comments below if you have callback hell in your current codebase, if you are considering checking this out or if you have any alternatives that work just as well in more complex codebases!