Async Await in Swift & SwiftUI

Async await is part of Swift concurrency. If you are new to concurrency, you should read this article first: Concurrency In Swift.

async/await allows us to write asynchronous code in Swift. It enables us to define asynchronous functions and manage concurrency in a way that looks like synchronous code. So code becomes more readable and maintainable. The async keyword used to define a function and the await keyword used to call an asynchronous function.

Defining an Asynchronous Function

To define an asynchronous function, we need to use the async keyword:

func fetchData() async throws -> String {
    // Simulate a network request
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2cseconds delay 
    let data = "Fetched Data"
    return data
}

If a function returns anything, we need to use the throws keyword. This function will return a String.

Calling an Asynchronous Function

To call an asynchronous function, use the await keyword. This must be done from within an asynchronous context. Here is an example of how to call an async function in a regular Swift program:

import Foundation

func fetchData() async throws -> String {
    // Simulate a network request
    try await Task.sleep(nanoseconds: 5 * 1_000_000_000) // 5 second delay
    let data = "Fetched Data"
    return data
}

let data = try await fetchData()
print("Data received: \(data)")

If we run the program, we will see Data received: Fetched Data in the console after 5 seconds.

In SwiftUI, we can’t directly call an asynchronous function. We need to use the Task or .task view modifier to call an asynchronous function from the SwiftUI main thread. Here is an example, where we have simulated a network call:

import SwiftUI

struct ContentView: View {
    @State private var data:String?
    
    var body: some View {
        VStack {
            if let data {
                Text("Data received: \(data)")
            } else {
                Text("Loading...")
            }
        }
        .task {
            do{
                data =  try await fetchData()
            } catch{
                print(error.localizedDescription)
            }
        }
    }
    func fetchData() async throws -> String {
        // Simulate a network request
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2 seconds delay
        let data = "Fetched Data"
        return data
    }
}

#Preview {
    ContentView()
}


Here, the .task view modifier is used to run asynchronous code when a view appears.

.task Modifier: The .task view modifier is a convenient way to run asynchronous code when a view appears. It ensures that the provided asynchronous work is tied to the view’s lifecycle.

Here is another example of how you can define and call an asynchronous function in SwiftUI. This time we used the Task modifier:

import SwiftUI

struct ContentView: View {
    @State private var data:String?
    
    @State var isLoading = false
    
    var body: some View {
        VStack {
            if let data {
                Text("Data received: \(data)")
            }
            if isLoading{
                ProgressView()
            }
        }
        
        Button("Fetch Data"){
            Task{
                isLoading = true
                do{
                    data = try await fetchData()
                }
                isLoading = false
            }
        }
    }
    func fetchData() async throws -> String {
        // Simulate a network request
        try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // 2 seconds delay
        let data = "Fetched Data"
        return data
    }
}

#Preview {
    ContentView()
}

In this example, we showed a ProgressView() while fetching data, like a regular app. Now let’s fetch real data from remote API using async/await.

Task: The Task type is used to create and manage units of asynchronous work. It can be used to start an asynchronous operation in various contexts, such as in a function or an event handler.

Example of Fetching Data from a Remote API

Here’s a simple example demonstrating the use of async and await in a SwiftUI app. This example will fetch data from a remote API and display it in a view.

import SwiftUI

struct Post: Identifiable, Codable {
    let id: Int
    let title: String
    let body: String
}
struct ContentView: View {
    @State var posts: [Post] = []
    
    var body: some View {
        NavigationStack {
            List(posts) { post in
                VStack(alignment: .leading) {
                    Text(post.title)
                        .font(.headline)
                    Text(post.body)
                        .font(.subheadline)
                        .foregroundColor(.secondary)
                }
            }
            .navigationTitle("Async Await Demo")
            .navigationBarTitleDisplayMode(.inline)
            .task {
                await  posts = fetchPosts()
            }
        }
    }
    
    func fetchPosts() async -> [Post]{
        
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return  []}
        
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            let fetchedPosts = try JSONDecoder().decode([Post].self, from: data)
            return fetchedPosts
        } catch {
            print("Failed to fetch posts: \(error)")
            return []
        }
    }
}

#Preview {
    ContentView()
}

Here,

  • An async function fetchData() that is used URLSession to fetch data from a network request.
  • We used.task modifier to call the async function from a non-async context. When the List view appears, it will call the fetchPosts() function.

Output:

When you run this example, the app will fetch a list of posts from the mock API (https://jsonplaceholder.typicode.com/posts) and display them in a list view. The task modifier ensures that the fetchPosts function is called when the view appears, and the posts are displayed once they are fetched.

Leave a Reply