Async/Await in Swift: Simplifying Asynchronous Code.


This is exciting, I’m eager to share what I’ve learned with some of the new APIs from 2021 WWDC like the whole new world of APIs for concurrent work.

The first thing to talk about within the scope of Concurrency is specifically about the introduction of async/await in Swift.

Following other Programming Languages like C# or JavaScript, Swift made his introduction into async/await in 2021.

In the latest post we talked about @Observable Macro, today let’s go a step back in Swift Evolution and explore a very important topic on Concurrency with Swift.

Let’s jump right in.


What is async/await?

In its simplest terms async/await are a couple of annotations to make your Asynchronous code looks like Synchronous code.

If you have been working with Swift for couple of years you probably know that working with completion handlers is not the most exciting part of programming at all.

Remembering to pass the completion at any given point in your Asynchronous function can be a pain.

Not to mention trying to avoid memory leaks while capturing values from the environment in your closures is another trap where you can fall as well.

Bugs start to ramp up, you have your suspects, but debugging them can be a lot of a challenge.


What we are going to do?

We are going to make a simple App to show a random dog image.

This is going to be simple, but a working example where you can get a lot of things from.

We are going to use SwiftUI, because is trendy, fast, and beautiful, and because is the way we developers are going to be coding for a long time. Ok, UIKit have it’s beauty, don’t get me wrong, but you know..

The thing is tapping a button to get a new random dog image each time.

To start with we will get the images from https://random.dog/woof.json. I didn’t know about this before writing this post, and looks cool!.

Paste the URL in your browser, you’ll get a JSON, use the url from the response and see what doggie you get, it’s cool!

Ok, now we need to make 2 things to get the image from it.

  • First we need to get the JSON.
  • After that we need to extract the URL from the JSON and make a new network call to get the final image with it.

The old way with Completion Blocks.

let’s take a look at this way of making a network calls using the previously mentioned completion callback closures that we’ve been using before async/await come in.

typealias DogImageResult = Result<UIImage, DogImageError>
func fetchDogData(completionHandler: @escaping(DogImageResult) -> Void) {
 
  guard let jsonURL = URL(string: "<https://random.dog/woof.json>") else { 
		completionHandler(.failure(DogImageError.invalidURL))
	  return
  }
    
  let session = URLSession.shared
  let task = session.dataTask(with: jsonURL) { data, response, error in
    guard let data = data, error == nil else {
      completionHandler(.failure(.networkError))
      return
    }
    
    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
      completionHandler(.failure(.invalidResponse))
      return
    }
    
    do {
      let dogPhotoResponse = try JSONDecoder().decode(DogPhoto.self, from: data)
      let imageUrl = dogPhotoResponse.url
      
      self.fetchDogImage(from: imageUrl, completionHandler: completionHandler)
    } catch {
      completionHandler(.failure(.decodingError))
    }
  }
	task.resume()
}

struct DogPhoto: Decodable {
  let url: String
}

enum DogImageError: Error {
    case invalidURL
    case networkError
    case invalidResponse
    case decodingError
    case imageDownloadError
}

Here we make a lot of things, and yes, just to get the JSON.

And while we wait for our JSON we may end up with some errors along the way.

Note a few additional types here that help us to write the network code: the DogImageError enum that can hold for error cases and a simple enough DogPhoto struct that mimics our expected JSON format; just the URL is only what we care about here.

We didn’t finish yet, once we get the JSON we need to make another network call to get the image itself.

func fetchDogImage(from imageUrl: String, completionHandler: @escaping(DogImageResult) -> Void) {
  if let imageUrl = URL(string: imageUrl) {
    let session = URLSession.shared
    let imageTask = session.dataTask(with: imageUrl) { imageData, _, error in
      if let imageData = imageData, error == nil {
        if let image = UIImage(data: imageData) {
          completionHandler(.success(image))
        } else {
          completionHandler(.failure(.imageDownloadError))
        }
      } else {
        completionHandler(.failure(.networkError))
      }
    }
    imageTask.resume()
  } else {
    completionHandler(.failure(.invalidURL))
  }
}

Wrap this 2 functions, fetchDogData() and fetchDogImage(), with the DogPhoto struct and DogImageError enum inside a class, call it whatever you want but if you’re following along I’ve called it DogDataVM.

Now take a look at how to make this work with our SwiftUI views:

struct ContentView: View {
  var vm = DogDataVM()
  @State private var dogImage: UIImage?
  
  var body: some View {
    VStack {
      DogImageView(dogImage: dogImage)
      
      Button("Fetch a random dog image") {
        getADog()
      }
    }
  }
  
  func getADog() {
    vm.fetchDogData { result in
      switch result {
      case .success(let image):
        DispatchQueue.main.async {
          dogImage = image
        }
        
      case .failure(let error):
        print(error)
      }
    }
  }
}

struct DogImageView: View {
  let dogImage: UIImage?
  
  var body: some View {
    if let dogImage {
      Image(uiImage: dogImage)
        .resizable()
        .scaledToFit()
    }
  }
}

Give it a try, it works, but, yes, it looks like a lot of boilerplate and something that you need to do a lot of times in order to commit to memory.. but it works.

And it have been working for a long time.


async/await to the rescue

Now let’s see how async/await can help us improve all this.

Let’s begin by making a new function in our View Model to make our new network call. Leave the other 2 functions alone, this will help you see how things changed.

func fetchDogImageWithAsyncAwait() async throws -> UIImage {

Look at this, now we are expecting a UIImage, and it makes sense, expecting Void or nothing is not, because we want something back.

Also, before the return type we have our new friend async followed by the throws keyword.

Very descriptive name BTW.

As I said at the beginning of the post now our func start to look more like a common and Synchronous function, expecting a very well defined return type, and also knowing that it can throw some error.

Let’s continue.

The first thing we need to start is the URL to get the JSON.

guard let jsonURL = URL(string: "<https://random.dog/woof.json>") else {
  throw DogImageError.invalidURL 
}

Here we get our first difference.

If we found a JSON, great, but if not, now we can throw an error instead of passing it with a CompletionHandler.

This is more clear, avoids the nesting part of the Error Handling and help us debug our code with much clarity.

Now comes the change that we are waiting for.

let (jsonData, response) = try await URLSession.shared.data(from: jsonURL)

Here we make use of the async/await version that URLSession provides.

This is the new way, you expect a tuple coming back from the call. As you would normally do with any other Sync code but this time with the await keyword.

The Async keyword indicates that this function can be paused and that the OS can free up resources (the thread) until it gets the response, to continue exactly where it’s left.

This operation may fail so the try keyword goes before the await.

Since the whole func is marked with throws the compiler is happy.

Now let’s work with the data that came back from it.

guard (response as? HTTPURLResponse)?.statusCode == 200 else {
  throw DogImageError.invalidResponse
}

First of, the response, if its 200 it’s all good, let’s continue, if not, throw an error instead of completing with it like before.

let dogPhotoResponse = try JSONDecoder().decode(DogPhoto.self, from: jsonData)

Now instead of wrapping the throwing func decode inside a do/catch block you simply wait for it with the same model that we were using before; DogPhoto.

Now let’s pull the URL of the image from the JSON response.

guard let imageURL = URL(string: dogPhotoResponse.url) else { 
	throw DogImageError.invalidURL
}

Now that we have what we cared about in our first call (the imageURL) let’s use the URL to make the new network call to bring the image.

let (imageData, imageResponse) = try await URLSession.shared.data(from: imageURL)

The same dance here, pretty simple and straight forward.

We will wait for a tuple, with the data and the response using the same as the call before to get the JSON but this time we’ll receive the image data and a response from the call.

guard (imageResponse as? HTTPURLResponse)?.statusCode == 200 else { 
	throw DogImageError.invalidResponse 
}

Now that we are sure that the response is legitimate we continue using the imageData to make our UIImage and then simply return in it (as our func signature expects)

guard let image = UIImage(data: imageData) else { 
	throw DogImageError.decodingError 
}

return image

Our new network call using async/await is complete.

Now let’s take a look and how we can call it in our view.

Inside our getDog() func let’s wrap our new call with a Task.

Task {
  do {
    dogImage = try await vm.fetchDogImageWithAsyncAwait()
  } catch {
    print("There is something wrong here, let's see: \(error.localizedDescription)")
  }
}

This Task allow us to make Async calls since if we need to call an Async function we need to mark it as await so Task is the way to go here.

Now, comment or delete the old vm.fetchDogData and give it a try.


Great, now you have an async/await func properly working, and there are a lot of them that are used to work with Completion Handlers and now are taking advantage with it’s newest async/await versions.

If you have being paying attention you probably noticed that what we do everything in 1 function instead of two functions like before with the old way.

Since the whole dance of getting the JSON, extract the URL and make another call to get the final imageURL is simple enough it makes sense to wrap all in one function.

Of course if you find an opportunity to decouple it go ahead, you can, also if you want to make 2 network calls with nested completion handlers in it you can do it as well so you can see for yourself and if you want to know how they look like, come on, give it a try if you are curious. Good luck with it though! 😄


Final Code with async/await

Let’s take a look at how the complete function looks like:

func fetchDogImageWithAsyncAwait() async throws -> UIImage {
  guard let jsonURL = URL(string: "<https://random.dog/woof.json>") else { 
		throw DogImageError.invalidURL 
	}
  
  let (jsonData, response) = try await URLSession.shared.data(from: jsonURL)
  guard (response as? HTTPURLResponse)?.statusCode == 200 else { 
		throw DogImageError.invalidResponse 
	}
  
  let dogPhotoResponse = try JSONDecoder().decode(DogPhoto.self, from: jsonData)
  guard let imageURL = URL(string: dogPhotoResponse.url) else { 
		throw DogImageError.invalidURL
  }

	let (imageData, imageResponse) = try await URLSession.shared.data(from: imageURL)
  guard (imageResponse as? HTTPURLResponse)?.statusCode == 200 else { 
		throw DogImageError.invalidResponse 
	}
  
	guard let image = UIImage(data: imageData) else { 
		throw DogImageError.decodingError 
	}
  
  return image
}

It’s not so much expressive, clear and concise than before? I think that we gain a lot with it.

Here goes the SwiftUI view:

struct ContentView: View {
  var vm = DogDataVM()
  @State private var dogImage: UIImage?
  
  var body: some View {
    VStack {
      DogImageView(dogImage: dogImage)
      
      Button("fetch random dog image") {
        getADog()
      }
    }
  }
  
  func getADog() {
    Task {
      do {
        dogImage = try await vm.fetchDogImageWithAsyncAwait()
      } catch {
        print("There is something wrong here, let's see: \(error.localizedDescription)")
      }
    }
  }
}

struct DogImageView: View {
  let dogImage: UIImage?
  
  var body: some View {
    if let dogImage {
      Image(uiImage: dogImage)
        .resizable()
        .scaledToFit()
    }
  }
}

In summary

Now, let’s summarize some of the key benefits of using async/await

  1. Simplicity. Using Asynchronous functions that look like Synchronous ones is much more simple to deal and to reason about. Writing more understandable code is always key.
  2. Better error handling. Since you are throwing errors and to complete in it the callback with them is far better to avoid forgetting it.
  3. Sequentiality: Every line of the func starts and completes, all seems to follow it’s proper order giving you the clarity needed to follow through.
  4. Avoid Callback Hell. I don’t want to put it in this words up until now, but if you made the test of nesting the callbacks won’t you felt some pain? Sure you do. Now it’s gone.
  5. Better debugging. All this boils down also to a much better debugging.

A word of advice:

In the world of work you will find this kind of scenario with the old fashioned completion blocks all over the place, is still the way to go in many codebases out there.

If you get this topic right you can make a very positive impact on your job, because you can start to polish existing code and make the life easier for you and for everyone.

Of course you can ask to your Scrum Master or to your Team Lead to place this in a Jira Ticket so you can visualize what you’re doing, or sometimes is a bold move if you can go and take time of yourself to go the extra mile, make it in your own time, put a PR out there to let the Team know that you care about what you do.

If you are brave enough to make it outside your regular duties, you’ll start to call the attention of different people inside and out of your organization.

Give it a try 😉

Let’s connect on LinkedIn

Thanks for reading! 🥸


Github repo with the working example: https://github.com/FeRHeDio/AsyncAwaitDemo


ref: https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/