Async/await in Swift and SwiftUI
Tags: swift, programming, networking, concurrency
Published: 2023-09-12 10:35:49

_This article first appeared on [matteomanferdini.com](https://matteomanferdini.com/swift-async-await/)_

## Chapter 1: What is async and await in Swift?

Async/await is a mechanism used to create and execute asynchronous functions in Swift.

* **async** indicates that a function or method is *asynchronous* and can pause its execution to wait for the completion of another process.
* **await** marks a *suspension point* in your code where execution may wait for the result of an async function or method.

###How to write an async function

To declare an asynchronous function in Swift, write the `async` keyword after the function name and before its return type.

    import Foundation
    
    func fetchImageData() async throws -> Data {
        let data = // ... Download the image data ...
        return data
    }

Whenever one of your functions calls another asynchronous method, it must also be declared as `async`.

###How to use await in Swift

You place the `await` keyword wherever you need to call an `async` function. It creates a suspension point where the execution of your code may pause until the asynchronous function or method returns.

As an illustration, let’s download an image using a URL from [the Dog API](https://dog.ceo/dog-api/).

    func fetchImageData() async throws -> Data {
        let url = URL(string: "https://images.dog.ceo/breeds/mountain-swiss/n02107574_1387.jpg")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }

## Chapter 2: Why do we need asynchronous functions in Swift and iOS apps?

An iOS app idly waits for input, such as the user tapping a button or data arriving from the network. When such an input arrives, it triggers an event in the app that causes your code to run. After that, the user interface must be updated.

### Blocking the main run loop for too long makes your app unresponsive

When an iOS app runs, it consistently cycles through a run loop consisting of three phases:

1. Receiving input events.
2. Executing code (possibly yours).
3. Updating the UI.

The cycle runs so swiftly that the app appears to respond instantly to user input. However if some code takes too long to execute, it delays the subsequent UI update and input phases. Your app may feel sluggish, freeze briefly, and lose input.

### Asynchronous functions can perform long tasks without blocking the app’s main run loop

When you call an asynchronous function, your code gets suspended, leaving the app's main run loop free. Meanwhile, the work performed by the asynchronous function runs "in the background".

The code running in the background can take as much time as it needs without impacting the app's main run loop. When it finishes and returns a result, the system resumes your code where it left off and continues executing it.

# Chapter 3: Structured and unstructured concurrency

A task is a unit of work that can be run asynchronously.

Tasks can be arranged hierarchically, allowing you to run several tasks in parallel. This approach is called *structured concurrency.* The easiest way to create child tasks is by using `async let`

Swift also allows you to explicitly create and manage tasks. This approach, in turn, is called *unstructured concurrency*.

### Calling async functions sequentially

We can create an async function to retrieve the URL for a random dog image and then download its data.

    struct Dog: Identifiable, Codable {
        let message: String
        let status: String
    
        var id: String { message }
        var url: URL { URL(string: message)! }
    }
    
    func fetchDog() async throws -> Dog {
        let dogURL = URL(string: "https://dog.ceo/api/breeds/image/random")!
        let (data, _) = try await URLSession.shared.data(from: dogURL)
        return try JSONDecoder().decode(Dog.self, from: data)
    }
    
    func fetchImageData() async throws -> Data {
        let url = try await fetchDog().url
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }

The `fetchImageData()` function makes two asynchronous calls sequentially.

### Running several asynchronous functions in parallel using structured concurrency

You can run a discrete number of asynchronous functions simultaneously by using `async` in front of `let` when declaring a constant.

    func fetchThreeDogs() async throws -> [Dog] {
        async let first = fetchDog()
        async let second = fetchDog()
        async let third = fetchDog()
        return try await [first, second, third]
    }

The `async let` keywords do not create a suspension point like `await`. We only use `await` at the end of the function when we need the content of all three constants to create the final array.

### Creating unstructured tasks to call async methods from synchronous code

To call async methods from synchronous code we run `async` functions inside a `Task`.

    Task {
        let data = try await fetchThreeDogs()
    }

The code surrounding a task remains synchronous, so the main execution is not suspended.

The `Task` type allows you to run async functions inside a Swift playground or a command-line Swift program. Far more common is calling async methods from SwiftUI.

# Chapter 4 Using async/await in SwiftUI

While it's not considered good practice to place async methods inside a SwiftUI view directly, many of these functions must still be triggered from SwiftUI code.

### Calling async methods when a SwiftUI view appears on screen

SwiftUI specifically provides the `task(priority:_:)` modifier for this purpose.

    struct ContentView: View {
        @State private var dogs: [Dog] = []
    
        var body: some View {
            List(dogs) { dog in
                // ...
            }
            .task {
                dogs = (try? await fetchThreeDogs()) ?? []
            }
        }
    }

The task modifier keeps track of the task it creates and automatically cancels it when the view disappears from the screen.

### Calling an async method when the user pulls to refresh or taps on a button

Create a `Task` in the trailing closure of a `Button`, or the trailing closure of the `refreshable(action:)` view modifier.

    struct ContentView: View {
        @State private var dogs: [Dog] = []
    
        var body: some View {
            List(dogs) { dog in
                // ...
            }
            .refreshable {
                Task { 
                    dogs = (try? await fetchThreeDogs()) ?? []
                }
            }
            .toolbar {
                Button("Reload") {
                    Task { 
                        dogs = (try? await fetchThreeDogs()) ?? []
                    }
                }
            }
        }
    }

# Chapter 5: Async/await vs. completion closures

Concurrency with async/await should be your primary choice for any new project. However, you might have an existing project using the old callback-based asynchronous approach.

### Using callback-based asynchronous functions with completion closures

Writing our `fetchDog()` function using that approach is much more complicated than using async/await.

    struct HTTPError: Error {
        let statusCode: Int
    }
    
    func fetchDog(completion: @escaping (Result<Dog, Error>) -> Void) {
        let dogURL = URL(string: "https://dog.ceo/api/breeds/image/random")!
        let task = URLSession.shared.dataTask(with: dogURL) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }
            if let response = (response as? HTTPURLResponse), response.statusCode != 200 {
                completion(.failure(HTTPError(statusCode: response.statusCode)))
                return
            }
            do {
                let dog = try JSONDecoder().decode(Dog.self, from: data!)
                completion(.success(dog))
            } catch {
                completion(.failure(error))
            }
        }
        task.resume()
    }

### Replacing completion closures with async/await

You can use continuations to wrap your old callback-based asynchronous functions and provide an async alternative instead of rewriting them all from scratch.

Xcode also helps you in the process. ⌥-click on the function name, and you will find three options in the *Refactor* contextual menu.

### Convert Function to Async

    func fetchDog() async throws -> Dog {
        let dogURL = URL(string: "https://dog.ceo/api/breeds/image/random")!
        return try await withCheckedThrowingContinuation { continuation in
            let task = URLSession.shared.dataTask(with: dogURL) { data, response, error in
                if let error = error {
                    continuation.resume(with: .failure(error))
                    return
                }
                if let response = (response as? HTTPURLResponse), response.statusCode != 200 {
                    continuation.resume(with: .failure(HTTPError(statusCode: response.statusCode)))
                    return
                }
                do {
                    let dog = try JSONDecoder().decode(Dog.self, from: data!)
                    continuation.resume(with: .success(dog))
                } catch {
                    continuation.resume(with: .failure(error))
                }
            }
            task.resume()
        }
    }

This option is helpful if you prefer to immediately replace all instances of the old function with the new Swift concurrency approach.

### Add Async Alternative

    @available(*, renamed: "fetchDog()")
    func fetchDog(completion: @escaping (Result<Dog, Error>) -> Void) {
        Task {
            do {
                let result = try await fetchDog()
                completion(.success(result))
            } catch {
                completion(.failure(error))
            }
        }
    }
    
    
    func fetchDog() async throws -> Dog {
        let dogURL = URL(string: "https://dog.ceo/api/breeds/image/random")!
        return try await withCheckedThrowingContinuation { continuation in
            // ...
        }
    }

Select this option to retain all calls to the old version while using the new `async` version in your new code.

### Add Async Wrapper

    @available(*, renamed: "fetchDog()")
    func fetchDog(completion: @escaping (Result<Dog, Error>) -> Void) {
        let dogURL = URL(string: "https://dog.ceo/api/breeds/image/random")!
        let task = URLSession.shared.dataTask(with: dogURL) { data, response, error in
            // ...
        }
        task.resume()
    }
    
    func fetchDog() async throws -> Dog {
        return try await withCheckedThrowingContinuation { continuation in
            fetchDog() { result in
                continuation.resume(with: result)
            }
        }
    }

Utilize this option if you wish to maintain your existing code unchanged. It will keep using the old implementation while you incorporate Swift concurrency for new code.

**P.S.** [You can find the full version of this post here.](https://matteomanferdini.com/swift-async-await/)