Quick Guide to Async Await in Swift

Everything you need to know about new Swift asynchronous features. Async await, main actor, task, async get, and possible use cases — all covered.

Swift concurrency meme, main thread meme
https://speakerdeck.com/handstandsam/kotlin-actors-no-drama-concurrency?slide=18

How to create asynchronous functions, run code in parallel, who is MainActor, what is the closures pyramid and how to get rid of it? Let's start.

Straight to the point

Swift 5.5 introduced built-in support for writing asynchronous and parallel code in a structured way. Asynchronous code can be suspended and resumed later, although only one piece of the program executes at a time.

Keyword async is used to mark function as asynchronous. That's it.

func downloadNames(fromServer name: String) async -> [String] {
    ... // some other tasks
    return data
}

But what does it really mean?

💥
The async function can be suspended in the middle of the execution when it’s waiting for something.

Here's how async functions can be called

let namesMain = await downloadNames(fromServer: "main")
let secondary = await downloadNames(fromServer: "secondary")

When you type await, the current execution is suspended, until an asynchronous call is finished.

💡
Suspension is never implicit or preemptive — each such place is marked with the await keyword.

Where to call async functions

As I said before, await suspends current execution. But there must be a structure underneath that can be suspended. You can't suspend a raw thread or the main thread, for example.

You opened Playgrounds, right?

💡
Do not use Swift Playgrounds to test new concurrency features as they are not fully supported yet

If you try to call an async function in an inappropriate place, you will see this error

😡
async call in a function that does not support concurrency

That's because an asynchronous function can be called only in:

  • Code in the body of an asynchronous function, method, or property.
  • Code in the static main() method of a structure, class, or enumeration that’s marked with @main.
  • Code in an unstructured child task

That's a lot of words.

For most developers, only the first and the last points make sense. Most of the places in your code do not support await. How to deal with that?

Tasks and TaskGroup

Task

To call an asynchronous function in a place that does not support concurrency, you need to create a concurrent task. You can use Task and TaskGroup to achieve that.

Task {
    let names = await downloadNames(fromServer: "main")
    ... // futher work
    ... // take over the world (asynchronously)

}

When you create an instance of Task, you provide a closure that contains the work for that task to perform. Tasks can start running immediately after creation and may not. You can create a task in another Task or other concurrent environments.

let handle = Task { // Creates asynchronous task
	let names = await downloadNames(fromServer: "main")
    
	Task { // Creates asynchronous task
		await save(names: names)
	}
    
	for name in names {
		print(name)
	}
}

After creating a task, you use the instance to interact with it — for example, to wait for it to complete or to cancel it. Tasks run independently from their handles.

💡
To cancel a task, you can throw an error, return nil, or return partially completed work.

Use Task.isCancelled to check if the current task was cancelled.

TaskGroup to group the tasks

TaskGroup lets you launch several tasks and wait for the completion of all of them. The order in which these tasks are completed is not defined.

How to create it?

TaskGroup is created through withTaskGroup(of:). You provide closure in which you spawn new tasks and perform operations on returned data.

let calculations = await withTaskGroup(of: Int.self) { group -> Int in
	group.addTask { 1 * 2 } // () -> Int
	group.addTask { 2 * 3 }
	group.addTask { 3 * 4 }
	group.addTask { 4 * 5 }
	group.addTask { 5 * 6 }

	var collected = [Int]()

	for await value in group {
		collected.append(value)
	}

	return collected
}
http://i.imgur.com/gyAFz.jpg

The group object inside closure conforms to AsyncSequence. It's just like a general sequence, but elements are generated asynchronously. To iterate over it you can use .next() method or for await ... in sequence.

It can be used to parallelize for loops, for example.

let calculations = await withTaskGroup(of: Int.self) {[works] group -> [Int] in
	for work in works {
		group.addTask { work() }
	}

	var collected = [Int]()

	for await value in group {
		collected.append(value)
	}

	return collected
}

That's great, but how to perform unrelated tasks concurrently without TaskGroup?

Subscribe and don't miss posts!

Async let, async get, concurrent execution

These features seem like a real power to me.

Imagine you need to load an article, and data stored on different services or URLs:

  • Article thumbnail
  • Article text
  • Related articles
  • Comments

And the most obvious way to load all data is to write such code

let thumbnail = await loadThumbnail(forPost: post)
let text = await loadArticleText(forPost: post)
let related = await loadRelatedArticles(forPost: post)
let comments = await loadComments(forPost: post)

And this is mighty concurrent code that will load needed information the fastest way. Right? Not really.

Execution of async await code visualisation

Code is still executed serially and assets are not loaded in parallel. Each step waits until data is loaded. You can spawn a task for every step, sure. But is it really a nice solution?

That's where async let and async get come in handy.

💡
Use async let to start asynchronous tasks in parallel and to wait for their completion only when data is actually needed.
async let thumbnail = loadThumbnail(forPost: post)
async let text = loadArticleText(forPost: post)
async let related = loadRelatedArticles(forPost: post)
async let comments = loadComments(forPost: post)

// take over the world (synchronously)

let postInformation = await Post(thumbnail, text, related, comments)
async let asynchronous explanation

Wow! Four tasks run in parallel.

As you see, all four tasks launch and start to run in the background. Then, Post object is created only when all four functions return. As you remember, suspension can happen only when you use await keyword.

Computed properties also can be async with async get

💡
Use async properties to load the object's computed information concurrently

Let our Post provide the ability to load full JSON data

class Post {
    ...
    
    var fullJsonData: String {
        get async throws {
            let (data, _) = try await URLSession.shared.data(from: jsonUrl)
            return String(bytes: data, encoding: String.Encoding.utf8)
        }
    }
}

And to efficiently load JSON data for several posts

async let jsones = [
	currentPost.fullJsonData,
    nextPost.fullJsonData,
    previousPost.fullJsonData
]

save(jsonData: await jsones)

Use cases and examples

Closures

First of all, If you have ever written concurrent code, you know that most APIs are closure-based. If you needed to write some complex networking code, you probably already constructed some kind of closure pyramid.

Now, you don't need to pass a closure to catch data after the completion of an asynchronous task. You can wait for it with await keyword and your code will no longer look like a pyramid.

💥
Also, you can just forget to call a callback and nothing will remind you of that

Networking

URLSession now supports async/ await!

Therefore, networking code now is so much easier to read

let (content, _) = try await URLSession.shared.data(from: url)
// some work
let (text, _) = try await URLSession.shared.data(from: url)
// some work
let (image, _) = try await URLSession.shared.data(from: url)

UI updates and MainActor

The first thing you discover when trying to search for asynchronous tasks in UI is that UI must be updated from the main thread. And this resulted in this kind of code

func someUIUpdatingFunction() {
	// generating ui for taking over the world
    DispatchQueue.main.async {
    	// updates
    }
    // adding emoji
    DispatchQueue.main.async {
    	// updates
    }
}

...

DispatchQueue.global().async{
	someUIUpdatingFunction()
}

That looks chunky and async / await code can do better.

Swift introduced @MainActor. It can be used on classes, functions, structs, properties, computed properties, closures, etc.

What it does is tells swift that operations marked with @MainActor must be executed on the main thread.

@MainActor
func someUIUpdatingFunction() {
	// generating ui for taking over the world
    // updates
    // adding emoji
    // updates
}

...

Task {
	await someUIUpdatingFunction()
}

Or, if used for classes or structs, it makes all method calls and property accesses be executed on the main thread.

@MainActor
class PostDisplay {

	func updateView() async {
    	// executed on the main thread
    }
    
}
💡
Use @MainActor in classes wisely. If only several methods require running on the main thread, then mark only them and not the whole structure.

Final notes

That's the end of the quick guide. Note that Swift's API is massive and there's still a lot to cover and elaborate on. Check references to get a better understanding of some topics.

Also, check out an article on Swift's subtle features if you are unfamiliar with less-known Swift functionality!

Alex Dremov | Top 7 Subtle Swift Features
Here, I collected Swift features that are less known and can be useful when you prepare for interviews or want to deepen your Swift knowledge.

References

Concurrency — The Swift Programming Language (Swift 5.6)
Modern Concurrency in Swift, Chapter 2: Getting Started With async/await
Go into more detail about how the async/await syntax and the cooperative asynchronous execution work. Additionally, it introduces the usage of “async let” to design concurrent code and the “Task” type which encapsulates asynchronous execution in the modern concurrency model.
swift-evolution/0296-async-await.md at main · apple/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swift-evolution/0296-async-await.md at main · apple/swift-evolution
swift-evolution/0306-actors.md at main · apple/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swift-evolution/0306-actors.md at main · apple/swift-evolution
Apple Developer Documentation

Structure

  1. To the point: how to write async functions
  2. Where you can call async functions
  3. Tasks and TaskGroup
  4. Async let, async get
  5. Use cases and examples

Subscribe to Alex Dremov

Don’t miss out the latest publications! Only useful posts for developers to bump up your professional skills
bestdeveloper@example.com
Jump In