Back

Wesley de Groot's Blog
async/await

async/await

Let's delve into the fascinating world of Swift's async/await.
This powerful feature, introduced in Swift 5.5, revolutionizes asynchronous programming, making it more intuitive and readable. Buckle up as we explore the ins and outs of async/await!

What is async/await?

At its core, async/await simplifies handling asynchronous tasks. It allows you to write asynchronous code that looks almost synchronous. Here's how it works:

  1. async Functions: You mark a function as async. Inside this function, you can use await to pause execution until an asynchronous operation completes.

  2. await Expression: When you encounter an await expression, the function suspends execution until the awaited task finishes. Meanwhile, other tasks can continue running concurrently.

Example 1: Fetching Data

Let's start with a common scenario: fetching data from a remote API. Imagine we have a callback-based function for fetching data:

func fetchData(completion: @escaping (Data?, Error?) -> Void) {
    let url = URL(string: "https://api.example.com/data")!
    URLSession.shared.dataTask(with: url) { data, _, error in
        completion(data, error)
    }.resume()
}

Now, let's convert this to an async function using async/await:

func fetchData() async throws -> Data {
    let url = URL(string: "https://api.example.com/data")!
    return try await URLSession.shared.data(from: url).0
}

In the async version:

  • We use try await to wait for the data to be fetched.
  • The error handling is cleaner, thanks to Swift's throws.

Example 2: Multiple API Calls

Imagine we have a callback-based function for fetching data:

func fetchUser(id: Int, completion: @escaping (User?, Error?) -> Void) {
    // Fetch user details...
}

func fetchPosts(for user: User, completion: @escaping ([Post]?, Error?) -> Void) {
    // Fetch posts for the user...
}

// Usage:

fetchUser(id: 1) { user, error in
  guard let user = user else {
    print(error)
  }
  // Guard for no error
  self.fetchPosts(for: user) { posts, error in
    // Process user and posts
  }
}

Suppose we need to fetch a user and then fetch their posts. In the callback-based world, this can get messy. But with async/await, it's elegant:

func fetchUser(id: Int) async throws -> User {
    // Fetch user details...
}

func fetchPosts(for user: User) async throws -> [Post] {
    // Fetch posts for the user...
}

// Usage
Task {
    do {
        let user = try await fetchUser(id: 1)
        let posts = try await fetchPosts(for: user)
        // Process user and posts
    } catch {
        // Handle errors
    }
}

Running multiple tasks concurrently

func fetchUser(id: Int) async -> User {
    // Fetch user details...
}

func fetchPosts(for userID: Int) async -> [Post] {
    // Fetch posts for the user...
}

// Usage
import Foundation

Task {
    async let user = fetchUser(id: 1)
    async let userPosts = fetchPosts(for: 1)

    await updateUI(user: user, userPosts: userPosts)
}

Wait/Sleep in a Task

Task {
    // Delay the task by 1 second:
    try await Task.sleep(nanoseconds: 1_000_000_000)

    // Perform our operation
    // ...
}

Cancellation

func fetchUser(id: Int) async -> User {
    // Fetch user details...
}

func fetchPosts(for userID: Int) async -> [Post] {
    // Fetch posts for the user...
}

// Usage
import Foundation

let task = Task {
    async let user = fetchUser(id: 1)
    async let userPosts = fetchPosts(for: 1)

    await updateUI(user: user, userPosts: userPosts)
}

// Cancel the task
task.cancel()

Difference between Task and Task.detached

Task is a structured concurrency task that runs within the scope of the current task.
It is automatically cancelled when the parent task is cancelled.

Task.detached is a detached task that runs independently of the current task.
It is not automatically cancelled when the parent task is cancelled.

Ensure that your code is (not)running on the main thread

Explicitly running on the main thread

Task { @MainActor in 

}

Doing heavy work (background) and then updating the UI (main thread/@MainActor)

Task.detached(priority: .userInitiated) {
    await someHeavyBackgroundOperation()
    await MainActor.run {
        // Perform UI updates
    }
}

Doing heavy work on the background using nonisolated and async.

private nonisolated func doHeavyWork() async -> Bool {
  sleep(10)
  return true
}

Task {
    let result = await doHeavyWork()
}

Limitations

  • async/await is available starting from Swift 5.5.
  • async/await is not available on all platforms. It is available on iOS 15, macOS 12, watchOS 8, and tvOS 15.
  • async/await is not available in Objective-C code.

Conclusion

By embracing async/await, you create a better development experience and write more efficient code. Say goodbye to callback hell and welcome a cleaner, more expressive way of handling asynchronous tasks in Swift. 🚀

Resources:
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html#async-await
https://developer.apple.com/documentation/xcode/improving-app-responsiveness

x-twitter mastodon github linkedin discord threads instagram whatsapp bluesky square-rss sitemap