Understanding async/await in Swift with URLSession Bytes

Posted by

At the time of writing this post WWDC 2022 was just on the horizon (and by the time I publish, its probably just passed).

With that in mind, I don’t mind saying that I’m only just getting round to having a proper play with async/await from WWDC 2021 (see my words of wisdom on Twitter here) and I’m totally fine with that…

Overview

Whilst I absolutely love this new streamline concurrency approach, there was one particular API that really made me smile and stood out as a great example of how much this will change the way we write our code.

For me, this was URLSession.bytes – streaming data asyncronsly from an event driven endpoint to your app.

Now, from a syntax point of view, comparing this to a standard async/await operation there’s not much difference (apart from the API being used). But for me, it’s more about what it’s going to achieve – the idea of opening up a connection and being able to continuously stream data to your app with syntax written so fluently – just pleases me dearly (yep, the small things).

What do we need

For this post, we’re going to need a backend event stream API, so I knocked something up alongside my current test API api.bobross.dev that streams a random quote every 3 seconds (more on how I set that up in another blog post… maybe).

So the URL we’ll be using for this post is events.bobross.dev and the event responses will look like this;

{
  "text": "I think there's an artist hidden at the bottom of every single one of us."
}

For the code part, we’ll use a simple Xcode Playground for now.

Preparing our Playground

Let’s start by building a Codable struct along side a Enum for our network responses;

// Standard Codable Struct
struct Quote: Codable {
    var text: String?
}

// Basic Network Error enum
enum ErrorResponse: Error {
    case invalidUrl
    case invalidResponse
    case invalidData
}

Next, let’s create and empty function within a class where all the magic will happen;

class ApiService {
        
    func streamStocks() async throws {
        
    }
    
}

And finally, we’ll create a Task to set off our operation;

// Wrap our call in "Task" to allow this asynchronous function
// to be called from Playgrounds 'Synchronous' flow
Task {
    try await ApiService().streamQuotes()
}

async/await – the difference

A couple of things you may have noticed when prepping the preceding code, is a couple of new keywords;

func streamStocks() async throws {
try await ApiService().streamQuotes()

That’s right… async in our function name and await in the call to our function. These act as the building blocks of async/await with async defining that the method is to run ‘asynchronously‘ and await – simply, ‘waiting‘ for the async function to do its thing (asynchronously 😝).

Let’s take a look at how we implement this in Swift 5.x – we’ll take this one line/section at a time.

We’ll start with building the URL (nothing new here);

guard let url = URL(string: "https://events.bobross.dev/") else {
    throw ErrorResponse.invalidUrl
}

Next, is where the cool bits start;

let (bytes, response) = try await URLSession.shared.bytes(from: url)

guard let httpResponse = response as? HTTPURLResponse,
        httpResponse.statusCode == 200 else {
    throw ErrorResponse.invalidResponse
}

You’ll notice the tuple we declare calls the URLSession.bytes API, but there’s two important observations here,

  1. The use of the await keyword preceding our API call
  2. And the omission of the closure after the call.

Async/Await’s approach removes a lot of nested syntax along with a clean and concurrent procedural flow. Now, let’s now take a look at how we handle and decode the data once received.

for try await line in bytes.lines {
    let quote = try JSONDecoder().decode(Quote.self, from: Data(line.utf8))
    
    guard let quote = quote.text else {
        throw ErrorResponse.invalidData
    }
    
    print("Bob says: \(quote)")
}

All in all the preceding examples looks pretty standards, however again notice the await keyword when iterating through bytes.lines – here is Swift’s new Concurrency mechanism handling the asynchronous stream functionally as appose to using a delegate method we would have used previously.

Now we’ve got everything setup, lets test this out…

Task {
    try await ApiService().streamQuotes()
}

Again we precede our function call with the await keyword and all going well, we should now see a steady output of quotes from our favourite painter;

Conclusion

I like it… Swift’s async/await just works, whilst some may say it removes the art of programming , where once we delicately crafted our architecture to handles blocks, callbacks, closures etc…

This new approach allows us to spend time crafting our code in other areas, such as wrapping our head around yet another new architectural paradigm that’s doing the rounds in twitter 😜.

Code sample for this article can be found on GitHub here.

Enjoy

C. 🥃

Leave a Reply

Your email address will not be published. Required fields are marked *