Swift 5.5: Replacing GCD With Async/Await

Multithreading! The mere word sends shivers up one’s spine. And if it doesn’t, it should.

Main thread and background threads. Code that runs asynchronously. Code that can run simultaneously with other code. Code that can run simultaneously with itself. Code that can share data across threads — possibly with disastrous consequences. Concurrency.

Multithreaded code is scary stuff. Subtle mistakes can have far-reaching consequences — consequences which might not even manifest themselves on all devices, or at all times. Bugs in your multithreaded code can be notoriously hard to track down; there might be a crash at a certain line of code, but the actual mistake may have happened somewhere else entirely.

Still, sometimes multithreading is inevitable. And for years, one of the main ways we’ve coped is with GCD (Grand Central Dispatch).

GCD is very brilliant in its way, and learning to use it well is a source of pride. But it’s clumsy too. One reason is that GCD is written in C (using Apple’s peculiar brand of C with function blocks). Apple disguises that fact in Swift with some slick renamification, but the mapping between Swift calls and C calls is direct, and the syntax is imposed directly by the underlying C APIs. Complex concurrency is tricky to express in GCD (dispatch groups, anyone?). What’s more, GCD can’t protect you from your own mistakes. You can create a race condition. You can create a thread explosion. GCD won’t stop you.

The most exciting and far-reaching announcement to come out of Apple’s WWDC 2021 is that there’s a whole new way of doing concurrency in Swift, starting in Swift 5.5. Overall, this new way is called structured concurrency. But most people call it by a shorthand title: async/await.

That name comes from a prominent, powerful language feature. The async/await language construct itself has a long history, going back (according to the Wikipedia article) at least to the C# language in 2011. It lies at the heart of Swift’s structured concurrency. And it lets you control and express multithreading in a cleaner and safer way than GCD.

Unlike GCD, async/await consists of pure Swift language features; this means that the compiler can help us use it properly. The resulting code is clear and easy to reason about, which is crucial when you’re doing something as tricky as multithreading.

Starting with the Xcode 13 beta, async/await is ready for beta testers to try out. Not all proposed features are in place as of this writing, and it’s possible that some bugs are present; but there’s enough of it working that one can happily get an active feel for it right now.

What’s it like to use async/await? In this article, I’m going to show you. I’ll start by talking about GCD, because that’s what we’re all used to. Then I’ll give a simple GCD use case; I’ll discuss what’s good and what’s bad about this particular way of performing our task. Next, I’ll pause to demonstrate a minor improvement in the code using the Combine framework. Finally, I’ll rewrite the code completely so as to use async/await. I think you’ll be amazed by the beauty and simplicity of how async/await tackles the same task.

A little history

When Apple originally introduced Grand Central Dispatch, it was a really exciting development. Here, at last, was an efficient, low-level way of rationalizing multithreaded code in terms of queues and tasks rather than threads. NSOperation and NSOperationQueue already existed (and still do), but GCD was much more convenient to use, and had none of their external overhead.

In practical terms, the appeal of GCD rests on two features:

  • When you make a DispatchQueue, it’s a serial queue. This means that no task in the queue can get started until all previously enqueued tasks have finished.

  • GCD is expressed mostly using Objective-C blocks and Swift anonymous functions, often referred to as closures. Thanks to this block-based mode of expression, local variables used in one queue are passed directly into the next, provided the second block is nested inside the first.

Together, those two features constitute a form of locking, helping the programmer control the amount of data sharing between threads. Nevertheless, there are some unfortunate downsides.

One problem is what I just said about nesting. When you do something in an async block on one thread, and you want to do something else in another async block on another thread after that, you have to nest the second block inside the first. As you switch multiple times between thread contexts — for example, you get yourself from the main thread onto a background thread, and then you need to come back to the main thread, and so on — this structure forces all your code to move deeper and deeper to the right, which can rapidly become ugly and illegible.

An even more insidious problem is that beginners tend not to understand asynchronous code in the first place — and GCD does nothing to help. In fact, it’s downright misleading. Code nested in an asynchronous block is executed later than all the code that surrounds it, including code that comes later on the page. This reversal of the order of time comes as a complete surprise to naive programmers, who naturally tend to expect that code execution will march down the page from top to bottom in an orderly fashion.

This means that you can’t return a value from a function that obtains that value asynchronously — because the function returns before the asynchronous code can even start getting the value. Beginners get very upset about this, and start talking desperate nonsense about “waiting” for the asynchronous code to end before returning from the surrounding function.

The standard solution is to use a completion handler. But completion handlers come with issues of their own. They’re not easy to write. You’re still not actually returning a value; rather, you are calling back into your code at some later, unknown, arbitrary time. And you can’t signal failure by throwing an error, because you can’t throw “into” a completion handler; that’s why so many completion handlers pass both an Optional error and an Optional result (or a Swift Result enum). Plus, completion handlers have a nasty habit of propagating themselves; you can wind up with quite a lot of them, with code inside one completion handler calling yet another completion handler, which can get ugly and confusing.

Incidentally, I’ve posted a series of three online articles trying to cure beginners of their misconceptions about GCD and asynchronous code:

The joy of async/await syntax, on the other hand, is that it makes those poor misguided beginners turn out to be right after all! With async/await, you do get to wait for asynchronous code to finish, you can return a value (or throw an error) directly from an asynchronously called function, and your code does march serially down the page in order of execution. This should eliminate a lot of mistakes and misunderstandings, and my rants about how to deal with asynchronous code using GCD can eventually be laid to rest.

Things take time

To illustrate in the simplest possible way, I’m going to repurpose an example that I’ve used for many years: getting a time-consuming calculation off the main thread.

Imagine that we have a view (a UIView subclass), which I’ll call a Mandelbrot view — meaning that it that knows how to draw a simple version of the Mandelbrot set into itself. Constructing the set is a time-consuming operation, involving some deep looping and calculation. Here’s the code. I didn’t write it; I found it online. It probably isn’t a particularly good way to draw the Mandelbrot set; but it is a good way to spend considerable time calculating!

let MANDELBROT_STEPS = 100 // or some other large number
func makeBitmapContext(size:CGSize) -> CGContext { // *
    var bitmapBytesPerRow = Int(size.width * 4)
    bitmapBytesPerRow += (16 - (bitmapBytesPerRow % 16)) % 16
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let prem = CGImageAlphaInfo.premultipliedLast.rawValue
    let context = CGContext(data: nil, 
        width: Int(size.width), height: Int(size.height), 
        bitsPerComponent: 8, bytesPerRow: bitmapBytesPerRow, 
        space: colorSpace, bitmapInfo: prem)
    return context!
}
func draw(center:CGPoint, bounds:CGRect, zoom:CGFloat, context:CGContext) {
    func isInMandelbrotSet(_ re:Float, _ im:Float) -> Bool {
        var fl = true
        var (x, y, nx, ny) : (Float, Float, Float, Float) = (0,0,0,0)
        for _ in 0 ..< MANDELBROT_STEPS {
            nx = x*x - y*y + re
            ny = 2*x*y + im
            if nx*nx + ny*ny > 4 {
                fl = false
                break
            }
            x = nx
            y = ny
        }
        return fl
    }
    context.setAllowsAntialiasing(false)
    context.setFillColor(red: 0, green: 0, blue: 0, alpha: 1)
    var re : CGFloat
    var im : CGFloat
    let maxi = Int(bounds.size.width)
    let maxj = Int(bounds.size.height)
    for i in 0 ..< maxi {
        for j in 0 ..< maxj {
            re = (CGFloat(i) - 1.33 * center.x) / 160
            im = (CGFloat(j) - 1.0 * center.y) / 160
            re /= zoom
            im /= zoom
            if (isInMandelbrotSet(Float(re), Float(im))) {
                context.fill (CGRect(x:CGFloat(i), y:CGFloat(j), width:1.0, height:1.0))
            }
        }
    }
}

How does the Mandelbrot view use that code? Well, to generate a bitmap context containing a drawing of the Mandelbrot set, we call those two methods in succession:

let bounds = self.bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let bitmap = self.makeBitmapContext(size: bounds.size)
self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)

The idea is that we can then call self.setNeedsDisplay() to make the view’s own native draw(_:) method get called. Suppose we’ve got a bitmapContext property of our own:

var bitmapContext: CGContext!

Then we can say:

self.bitmapContext = bitmap
self.setNeedsDisplay()

Our draw(_:) method will then be called, and we can copy the bitmap context as an image into our own graphics context:

override func draw(_ rect: CGRect) {
    if self.bitmapContext != nil {
        if let im = self.bitmapContext.makeImage() {
            context.draw(im, in: self.bounds)
        }
    }
}

Why multithreading

So far, so good. But the core code here, the call to self.draw(center:bounds:zoom:context:), is time-consuming. And performing time-consuming code on the main thread is bad. It locks up the user interface; the user cannot interact with the app, and nothing appears to be happening. If this goes on for long enough, in fact, the iOS WatchDog process will kill our app dead before the user’s very eyes.

So we’d like to perform the time-consuming calculation code on a background thread. However, to refer to self.bounds, we need to be on the main thread. And to set self.bitmapContext and call self.setNeedsDisplay(), we need to be on the main thread too. So we’ve got a succession of context switches:

// on the main thread:
let bounds = self.bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)

// on a background thread:
let bitmap = self.makeBitmapContext(size: bounds.size)
self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)

// on the main thread:
self.bitmapContext = bitmap
self.setNeedsDisplay()

How are we going to do that?

Using GCD

With GCD, switching threads as we move down through the code depends, as I said before, on nested closures. First, you declare a background serial queue, typically as an instance property:

let draw_queue = DispatchQueue(label: "com.neuburg.mandeldraw")

Then, when it’s time to draw the Mandelbrot set, you just keep calling async on some dispatch queue — either that background serial queue, or the main queue — every time you want to switch threads. Each call to async takes a function that is typically expressed directly as a closure. And those functions are nested:

let bounds = self.bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
self.draw_queue.async {
    let bitmap = self.makeBitmapContext(size: bounds.size)
    self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)
    DispatchQueue.main.async {
        self.bitmapContext = bitmap
        self.setNeedsDisplay()
    }
}

Now, please don’t get me wrong. That code is simple and direct, and I happen to think that in its way it is absolutely delightful. I remember how excited I was about that sort of thing when GCD was first announced. It was so much better than what we had to do previously, using NSOperation or (horror of horrors) manipulating threads directly. Here are some reasons why:

  • We don’t have any code sharing across threads, because the variables are all local. Swift functions are closures, and a variable declared before the start of an anonymous function is captured by that function. So center and bounds just magically “fall through” into the background queue code, and then bitmap just magically “falls through” into the final main thread code.

  • The call to the time-consuming self.draw(center:bounds:zoom:context:) is on a background thread, and moreover, it’s on a serial queue. So, no matter how many times this code may run in quick succession, we will automatically “wait” until one call has finished before starting on another one; a serial queue is a form of data locking.

  • It is crucial that calls to UIKit interface properties and methods, such as self.bounds and self.setNeedsDisplay, be made on the main thread. Indeed, nowadays the Main Thread Checker will ding us if we don’t adhere to that rule. Well, we are able to confine those calls to the blocks that are on the main thread, and therefore all will be well.

  • Our own property, self.bitmapContext, is in danger of incoherence if it is touched from multiple threads (also known as a data race). We are able to be careful about that, too, touching self.bitmapContext only on the main thread and eliminating the danger.

All the same, this kind of thing is hard to write and hard to read. The nesting, in particular, threatens to get out of hand; if you have several context switches in a row, the nesting can get really deep. I’ve experienced that, and it isn’t pretty. Plus, you have to give some careful thought as to how the local variables are passed down into the deeper levels of nesting. And you still have to be extra vigilant about data races and the main thread. In other words, yes, we wrote some nice code, but it was sort of exhausting, and one still has a sense that maybe unknown problems remain.

Finally, beginners, in particular, don’t always grasp that for this to work, each async call needs to be the last thing in its block, because any code after the async block will run before the async block — that’s what “asynchronous” means.

Interlude: a Combine rewrite

Before I get to how we might do all this with async/await, I’d like to tell you about an intermediate solution using the Combine framework. I was really intrigued when Combine was introduced in iOS 13, and I spent a lot of time adapting my GCD code to use it. (If you don’t know about the Combine framework, you might like to look at my online book about it.)

The Combine framework expresses stages of a procedure as steps in a pipeline, known as operators. With Combine, the way to switch threads (queues) is the receive(on:) operator. And the way to do some arbitrary thing with the arriving input value and pass some arbitrary output value on to the next operator is with the map operator. So instead of GCD async, we can use Combine receive(on:) and map, like this:

.receive(on: DispatchQueue.main)
.map { () -> (CGPoint, CGRect) in
    let bounds = self.bounds
    let center =  CGPoint(x: bounds.midX, y: bounds.midY)
    return (center:center, bounds:bounds)
}
.receive(on: draw_queue)
.map { (center, bounds) -> CGContext in
    let bitmap = self.makeBitmapContext(size: bounds.size)
    self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)
    return bitmap
}
.receive(on: DispatchQueue.main)
.map { (bitmap:CGContext) -> () in
    self.bitmapContext = bitmap
    self.setNeedsDisplay()
}

Now, I’ve left out a bunch of details here. That code is just a sequence of Combine operators; to be useful, that sequence has to form part of an overall pipeline with a publisher at the start and a subscriber at the end — and I’ve omitted that part of the code. But forget about that for a moment and just look at the sequence. Notice anything? It’s flat! This code is really easy to read and really easy to write. It just goes step by step down the page, and it says exactly what it means:

  1. Make sure you’re on the main queue.
  2. Get the center and bounds and pass them on down the pipeline.
  3. Get on the draw_queue queue.
  4. Create and populate the bitmap context and pass it on down the pipeline.
  5. Get back on the main queue.
  6. Set our bitmap property and ask for a redraw.

I admit that my Combine code is more verbose than GCD — especially the way I write Combine code, since I really like to be very explicit about the incoming and outgoing types in a map function. But I actually like that verbosity, exactly because it’s explicit. Nothing just magically “falls through” to the next step; we have to pass values down the pipeline. Each map call says clearly what it is passing, and the next map calls says clearly what it is receiving. Pretty neat!

Nonetheless, even after converting my code to Combine, I’m still using GCD. The parameters in my receive(on:) calls are GCD dispatch queues. And although my Combine code is delightfully flat, nevertheless it’s arguably top-heavy. But it was the best I could do — until async/await arrived on the scene.

Watch your language

The idea of the async/await language construct is that you get to do exactly what I was saying you can’t do with GCD: you can wait for asynchronous code to finish.

In brief, the rule is this: If a function is marked async, then when you call it, you have to say await — and this is a signal that your code will pause on that line while the asynchronous call is performed. Later, when the asynchronous call finishes, your code resumes at the next line after the await, marching down the page like ordinary code.

I first heard about async/await through JavaScript. Naturally, like many other people, I immediately thought to myself: Swift needs this! Fortunately, the Swift people themselves were thinking the same thing, only on an even grander scale. Chris Lattner, who invented Swift to begin with, has been saying for years that Swift needs built-in language-level support for grappling with multithreaded code; as early as 2017 he had posted an extensive concurrency manifesto laying out the direction that this might take.

Actual proposals soon began to make their way through the Swift language evolution process, and so we arrive now at the Swift 5.5 beta, released as part of WWDC 2021, which showcases a whole suite of new native Swift language features related to multithreading.

My Mandelbrot example is actually far too simple-minded to illustrate the full power of Swift 5.5’s innovations. But for that very reason, I think it’s a good starting place for pedagogical purposes. Let’s convert my original GCD code to use async/await!

An actor prepares

I’m going to start in what may seem rather an odd place — with an actor. An actor, in Swift 5.5, is an object type, on a par with enums, structs, and classes. Its primary purpose, according to the WWDC 2021 videos, is to protect shared state. But the way I look at it, an actor does for modern Swift concurrency what queues did for GCD.

All actors are divided into two types: the main actor, and all others. The main actor isn’t something you make; it is an invisible global actor. A type (or method) that is marked as belonging to the main actor wants to run its code on the main thread. UIKit types such as UIViewController and UIView come already marked as belonging to the main actor, thus helping to ensure that their code will run on the main thread. All other actors, actors that you create, want to run their code on a background thread.

So this is how we’re going to express the idea that our calculations should be performed on a background thread: we’re going to create an actor type embodying those calculations, like this:

class MyMandelbrotView : UIView {
    var bitmapContext: CGContext!
    actor MyMandelbrotCalculator {
        private let MANDELBROT_STEPS = 100 // or whatever
        func drawThatPuppy(center:CGPoint, bounds:CGRect) -> CGContext {
            let bitmap = self.makeBitmapContext(size: bounds.size)
            self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)
            return bitmap
        }
        private func makeBitmapContext(size:CGSize) -> CGContext {
            // ... create context ...
            return context!
        }
        private func draw(center:CGPoint, bounds:CGRect, zoom:CGFloat, context:CGContext) {
            // ... operate on context ...
        }
    }
    let calc = MyMandelbrotCalculator()
    // ...
}

The beautiful thing about this architecture is that we now have an object with just one public entry point: drawThatPuppy. It takes the initial center and bounds values, and returns the fully drawn CGContext portraying the Mandelbrot set. Of course, we could have done all that with an ordinary struct. But remember, with an actor, we are also implicitly saying that our code should run on a background thread.

And here’s the really amazing part: there’s an additional rule, which will be enforced for us automatically, that only one outside call to an actor method can be running at any one time. In other words, the actor behaves much like a GCD serial dispatch queue!

Thanks to the actor, our object architecture is aligned with our threading architecture. Instead of having some methods that run on the main thread and others that run on a background thread, we have objects that operate on the main thread and objects that operate on a background thread.

Asyncing feeling

So I’ve now given my view a property, calc, which is an instance of this actor. To ask for the calculation to be performed, I’ll call into the actor’s public method. I’ll do that from a different drawThatPuppy method, belonging to the view:

func drawThatPuppy() {
    let bounds = self.bounds
    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    self.bitmapContext =
         self.calc.drawThatPuppy(center: center, bounds: bounds)
    // ...
}

We reach this point, and we are forced to stop; there’s a compiler error. But that’s not bad; it’s good! The compiler is going to help us with the rest of our code. The error message at this point is a little mysterious: it says, “Actor-isolated instance method drawThatPuppy(center:bounds:) can only be referenced from inside the actor.” What that really means is: Where’s the asynchronous part of all this?

Well, where is the asynchronous part? If this drawThatPuppy method is just an ordinary method of the UIView, it’s going to be called on the main thread. So now we are trying to call a method of the actor on the main thread. But this actor, you remember, wants to run on a background thread. We need to give the compiler permission to step off the main thread and call the actor on a background thread instead.

One way to do that, in Swift 5.5, is to mark the surrounding method as async, like this:

func drawThatPuppy() async { // *
    let bounds = self.bounds
    let center =  CGPoint(x: bounds.midX, y: bounds.midY)
    self.bitmapContext =
         self.calc.drawThatPuppy(center: center, bounds: bounds)
    // ...
}

Fine, but now the compiler has a new complaint: “Expression is async but is not marked with await.” And it offers a Fix-It. We accept the Fix-It, and this is what we get:

func drawThatPuppy() async {
    let bounds = self.bounds
    let center =  CGPoint(x: bounds.midX, y: bounds.midY)
    self.bitmapContext =
         await self.calc.drawThatPuppy(center: center, bounds: bounds) // *
    // ...
}

All right, fine, our code now compiles. Let’s finish writing this method; we just need one more line, the call to self.setNeedsDisplay:

func drawThatPuppy() async {
    let bounds = self.bounds
    let center =  CGPoint(x: bounds.midX, y: bounds.midY)
    self.bitmapContext =
         await self.calc.drawThatPuppy(center: center, bounds: bounds)
    self.setNeedsDisplay()
}

Excellent. Our code still compiles. But now we can’t run the code. Our code has no place to stand.

Taking a stand

To see what I mean, let’s say there’s a button in the interface that calls an action method that is supposed to call drawThatPuppy:

@IBAction func doButton (_ sender: Any) {
    self.mv.drawThatPuppy() // self.mv is the Mandelbrot UIView
}

We get a new compiler error: “async call in a function that does not support concurrency.” And the compiler offers us a Fix-It: we can mark this method as async as well:

@IBAction func doButton (_ sender: Any) async { // NO!

But we mustn’t do that! This is a method that Objective-C is going to call for us when the button is tapped; Objective-C here has no idea how to call this action method as an async Swift method. What are we going to do now?

This is a problem that first-time users of Swift 5.5 encounter right away. You convert a method to be async, but then you have to convert the caller of that method to be async, and then you have to convert the caller of that method to be async, and so on. The chain never seems to stop. But somewhere, it must stop; we need a place to take a stand within the good old world of normal methods, a place where we can say: This is not an async method, even though it needs to call an async method.

The main way you do that, in Swift 5.5, is with an async block, like this:

@IBAction func doButton (_ sender: Any) {
    async { // *
        self.mv.drawThatPuppy()
    }
}

Now we get our await compiler error once again: “Expression is async but is not marked with await.” We accept the Fix-It again, and we end up with this:

@IBAction func doButton (_ sender: Any) {
    async {
        await self.mv.drawThatPuppy() // *
    }
}

Our code now compiles! Not only that, it runs. Not only that, it runs correctly.

Pull on a thread

What do I mean by saying that the code runs correctly? What does this code actually even do?

Let’s take stock. We start with a normal doButton action method. It has an async block, which calls an async method, our Mandelbrot view’s drawThatPuppy, using the word await. Our Mandelbrot view’s drawThatPuppy, which is an async method, calls an actor method self.calc.drawThatPuppy, using the word await. And that’s all.

At this point you should be rounding upon me in fury and saying: “What do you mean, that’s all? Where’s the threading? Where’s the context switching? Where are the queues? We set out to run some code in the background, remember? What happened to that?”

But keep your shirt on! I assure you, we’ve taken care of all that. Return once again to the Mandelbrot view’s drawThatPuppy:

func drawThatPuppy() async {
    let bounds = self.bounds
    let center =  CGPoint(x: bounds.midX, y: bounds.midY)
    self.bitmapContext =
        await self.calc.drawThatPuppy(center: center, bounds: bounds)
    self.setNeedsDisplay()
}

All of that code runs on the main thread, because a UIView is marked as belonging to the main actor. So all the right things happen. We fetch our bounds on the main thread. We set our bitmapContext property on the main thread. We call setNeedsDisplay on the main thread. All very right and proper and necessary.

But the call to self.calc.drawThatPuppy runs on a background thread, because self.calc is a background actor! Do you see the point? We don’t have to talk about threads at all! Because of our object architecture, the right thing happens, all by itself.

Okay, but wait a moment. How can that be? self.calc.drawThatPuppy runs on a background thread and takes a lot of time. Meanwhile, why doesn’t our code just proceed merrily on its way on the main thread? Oh, I can answer that one. It’s because of the word await! This word means just what it says: Stop and wait for this call to complete, no matter how long it takes.

But now, I’m guessing, you probably want to say to me: “Stop and wait? When you’re on the main thread?? Are you crazy? You’re blocking the main thread! Everyone knows that’s a first-class crime!” No. Get ready for this: Waiting does not block. That’s not what await does.

When we reach the word await, the code is suspended, but the thread is not. The main thread just walks off and leaves that code in a frozen state, and continues to run. The user can continue to interact with the interface, other code can run on the main thread, and so forth. When the background work finishes, the background thread, in its turn, submits its result and goes on to other things. Perhaps at that moment the main thread is busy doing something else; no problem. Sooner or later, when the main thread is free, it receives back the result, and our code continues on its way once more from where it left off.

And that, of course, is the other amazing thing about this code. The call to self.calc.drawThatPuppy returns a value — across the context barrier between one thread and another. That is exactly the kind of thing that GCD can’t do — because it can’t wait.

We thus end up with the most natural-looking code imaginable. The code just steps through its lines one at a time in order. The third line of drawThatPuppy is really not all that special. We call a method that returns a value, and we get back that value, just like normal code. And yet, in that one line, the path of execution crosses from the main thread to a background thread, performs time-consuming work on the background thread, and comes back again to the main thread.

Moreover, this code is safe and orderly. Recall how we hooked everything up to a button in the interface. The user can tap that button several times in a row. The first time, our actor isn’t doing anything, and the calculation code starts to run immediately (on a background thread). But each time after that, the actor is busy! This means that the await lines all just have to wait until the actor is finished so that it can begin processing another calculation. But none of that affects the main thread or the interface; the user can keep working, and eventually the view will update with each new drawing as it finishes. That’s what I mean when I say that our actor is like a serial queue.

What we learned

In this very simple example, we have just dipped our toes into the world of async/await. Yet we have experienced, in action, the key basis of the new Swift concurrency regime. What did we find out?

  • The main actor is a global object that wants its code to be run on the main thread. UIKit objects like UIViewController and UIView belong to the main actor, so they want their code to be run on the main thread.

  • An actor that you create is an object that wants its code to be run on a background thread.

  • An actor that you create also has the special power that its code cannot be called and running from the outside multiple times simultaneously, so there is no danger of simultaneous access.

  • Because of the actor’s special power, code that calls into an actor from outside might have to wait (because that code might be running already). Therefore, such a call must be marked with await.

  • A call marked with await will pause at the point of the call, waiting for the call to be completed. This means that the call can return a value to the waiting caller.

  • Code that says await must itself be asynchronous, so that it is capable of waiting. There are two primary ways in which code can be made asynchronous:

    • An entire method can be declared async. Such a method can only be called with await.

    • A block of code can be declared async by making it an async {...} block. That code can be in a method that is not itself asynchronous.

The example I used to demonstrate all this was deliberately very simple-minded. There are only two concurrent things happening: the main thread, and the background calculation of the Mandelbrot set. In my implementation, only one such background calculation can happen at a time; if the user asks for two calculations in quick succession, the second calculation cannot even start until the first has completed. That’s perhaps not a very efficient use of the device’s resources! But it does demonstrate how easily and simply such a thing can be configured.

A new world

Now that you know about the async/await syntax, you’re ready to begin using it immediately. You don’t even have to write any new code; you can adapt your old code. Apple has already supplied new versions of some of its most popular asynchronous methods; those new versions use async/await instead of a completion handler. And you can start calling them!

Take, for example, networking. You are probably used to calling this URLSession method:

func dataTask(with url: URL, 
    completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) 
    -> URLSessionDataTask

You obtain the data task, call resume on it, and receive the response in a completion handler that you supply. The completion handler takes three parameters: the data received from the network, the response, and also an Error object in case there is an error (because completion handlers can’t throw). Any of those might be missing — if there’s an error, there’s no data, but if there’s data, there’s no error — so they are all Optional and you have to test and unwrap them. You know the drill.

Instead, you can now call this method:

func data(from url: URL, delegate: URLSessionTaskDelegate? = nil) async throws 
    -> (Data, URLResponse)

That’s an async method. You call it with await — actually, you would have to say try await because it can throw to signal an error — and it returns a tuple of the data and the response directly to you, the caller. There’s no resume. There’s no completion handler. And there’s no Error value returned to you, because if there’s an error, the called method can throw!

Here’s a basic example of calling the new async method:

@MainActor func doSomeNetworking() async {
    guard let url = URL(string:"https://www.apeth.com/pep/manny.jpg") else {return}
    do {
        let (data, resp) = try await URLSession.shared.data(from: url)
        guard (resp as? HTTPURLResponse)?.statusCode == 200 else {
            print("bad response")
            return
        }
        if let image = UIImage(data: data) {
            self.imageView?.image = image
        }
    } catch {
        print(error)
    }
}

Here are some things to notice about that code.

We have to say await as we call URLSession.shared.data(from: url), which means we need to be in an async context; in my implementation, the whole surrounding method (doSomeNetworking) is marked async. But the method we are calling is also a throws method, so we have to obey the rules about that, too; in addition to await, we also say try, and we can only do that in an appropriate context — either in a do...catch construct, or in a throws method. Here, I’ve opted for the former.

There is no callback into our completion handler; we have no completion handler! Instead, after the await call to URLSession.shared.data(from: url), the code, which was paused, just resumes and carries straight on. If the call doesn’t throw, the results have been simply returned directly to us as a tuple. I check both results in turn. If the response is good and the data turns out to be image data, as expected, I assign it to an image view property as its image. But that requires that I be on the main thread; to ensure that, I’ve marked this method as @MainActor. Pretty easy!

Asynchronous sequences

Another intriguing update to the existing APIs is that some things are now available as asynchronous sequences. An asynchronous sequence is rather like the stream of values that arrive from a Combine framework publisher: the values arrive whenever they happen to arrive. To receive them, you can loop over the sequence asynchronously.

A case in point is notifications from the NotificationCenter. In this test code, we arrange to receive notifications called "testing" sent by self:

let notif = NotificationCenter.default
let notifs = notif.notifications(
    named: Notification.Name(rawValue: "testing"),
    object: self)
for await n in notifs {
    // n is a notification! do something with it
}

The surprise here is the expression for await. It says that each n in the sequence can arrive at any old time, so we will just pause looping until the next one actually does arrive. We are saying await, so this is code which, once again, can only live in an async context.

Similarly, a URLSession now offers a bytes method, which returns an asynchronous sequence that itself has a lines method that also returns an asynchronous sequence. Looping over this with for await, you can receive and process downloaded data one line at a time.

What we didn’t learn yet

You now know more than enough to get started with async/await! As I’ve suggested, a good approach is to find some asynchronous calls in your existing code, and switch over to an async/await alternative.

So what didn’t we learn yet? Lots! Here are some of the topics that call for further exploration:

  • What if we need to cancel an asynchronous task?

  • How do we dictate the priority with which an asynchronous task should be executed?

  • How do we spawn multiple simultaneous asynchronous tasks?

  • What if we have to call into code that uses a completion handler? How can we incorporate that code into the await/async architecture?

  • How do we write unit tests for the new style of asynchronous code?

My plan is to introduce those topics in a later article.

One word of caution. As far as I can tell, async/await isn’t just language-bound but iOS version-bound. If you have code that needs to run on a system earlier than iOS 15, which appears in beta in Xcode 13, you won’t be able to retire your GCD and completion handler–based code. Nevertheless, using Swift’s availability checking, you can experiment with the async/await alternatives.

Also, as I’ve implied already, these features are not yet fully baked. There may be bugs (I believe I’ve found some), and things may change quite a bit before it all goes final. So keep an open mind, stay flexible, and enjoy the splendid new world of Swift async/await.

You Might Also Like…

Swift 5.5: Asynchronous Looping With Async/Await

In an earlier article, I introduced the Swift 5.5 async/await syntax that lies at the heart of Swift’s new structured concurrency. To do so, I demonstrated how to replace a common use case of Grand Central Dispatch (GCD) — switching from the main thread to a background thread and then back to the main thread …

Swift 5.5: Asynchronous Looping With Async/Await Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.