Swift async/await Examples for Asynchronous Programming

Explore practical examples of using Swift's async/await for effective asynchronous programming in your projects.
By Jamie

Introduction to Swift’s async/await

Swift’s async/await syntax simplifies asynchronous programming, allowing developers to write cleaner and more readable code. This feature, introduced in Swift 5.5, enables you to handle asynchronous tasks without the complexity of traditional callback mechanisms. Here are three diverse, practical examples that demonstrate how to use async/await in Swift.

Example 1: Fetching Data from a REST API

In this example, we will fetch user data from a REST API. This is a common use case when building apps that require data from external sources.

To fetch data asynchronously, we define an asynchronous function that uses the URLSession API.

import Foundation

struct User: Codable {
    let id: Int
    let name: String
}

func fetchUserData() async throws -> User {
    let url = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
    let (data, _) = try await URLSession.shared.data(from: url)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

Task {
    do {
        let user = try await fetchUserData()
        print("User: \(user.name)")
    } catch {
        print("Error fetching user data: \(error)")
    }
}

This code defines a User struct that conforms to Codable, making it easy to decode JSON data. The fetchUserData function performs a network request and decodes the response into a User object.

Notes

  • Ensure that the API endpoint is accessible and returns valid JSON.
  • Handle errors gracefully, as shown in the do-catch block.

Example 2: Parallel Execution of Tasks

In this example, we will demonstrate how to run multiple asynchronous tasks in parallel. This can improve the performance of your applications when tasks are independent of each other.

We’ll fetch posts and comments from the API simultaneously.

import Foundation

struct Post: Codable {
    let id: Int
    let title: String
}

struct Comment: Codable {
    let id: Int
    let body: String
}

func fetchPosts() async throws -> [Post] {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Post].self, from: data)
}

func fetchComments() async throws -> [Comment] {
    let url = URL(string: "https://jsonplaceholder.typicode.com/comments")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Comment].self, from: data)
}

Task {
    do {
        async let posts = fetchPosts()
        async let comments = fetchComments()
        let (fetchedPosts, fetchedComments) = try await (posts, comments)
        print("Fetched \(fetchedPosts.count) posts and \(fetchedComments.count) comments.")
    } catch {
        print("Error fetching data: \(error)")
    }
}

In this code, we use async let to fetch posts and comments concurrently, allowing both operations to run simultaneously.

Notes

  • Parallel execution is beneficial for tasks that don’t depend on each other.
  • Keep an eye on your app’s performance and responsiveness when running multiple tasks.

Example 3: Handling User Input with Async Functions

This example illustrates how to handle user input asynchronously in a SwiftUI application. We will simulate a user login process with a delay to represent a network call.

import SwiftUI

struct ContentView: View {
    @State private var username: String = ""
    @State private var isLoggedIn: Bool = false
    @State private var errorMessage: String = ""

    var body: some View {
        VStack {
            TextField("Username", text: $username)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
            Button("Login") {
                Task {
                    await loginUser(username: username)
                }
            }
            .padding()
            if isLoggedIn {
                Text("Welcome, \(username)!")
            } else if !errorMessage.isEmpty {
                Text(errorMessage).foregroundColor(.red)
            }
        }
        .padding()
    }

    func loginUser(username: String) async {
        do {
            // Simulate a network delay
            try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
            // Basic validation
            if username.isEmpty {
                throw NSError(domain: "LoginError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Username cannot be empty."])
            }
            // Simulate successful login
            isLoggedIn = true
        } catch let error as NSError {
            errorMessage = error.localizedDescription
        }
    }
}

In this SwiftUI example, we create a simple login form. When the user presses the login button, the loginUser function is called asynchronously, simulating a network operation with a delay.

Notes

  • Use Task.sleep to simulate delays for demonstration purposes; in a real application, you would replace this with actual network calls.
  • Ensure that error handling provides clear feedback to the user.

These examples demonstrate the versatility and power of Swift’s async/await syntax in various scenarios, making asynchronous programming more intuitive and maintainable.