Concurrency in modern Swift with Async & Await in SwiftUI — Part 2

DevTechie Inc
Jul 17, 2023

We will continue our exploration of modern concurrency with async and await.

Async and Await

The main idea behind async await is to offer support for asynchronous task execution without blocking the UI for the user. For example, if we have an app that displays an image from the internet and also has a counter, a user can tap on it to increase the counter while waiting for the app. If we make the call to download image from internet on main thread, the counter will not work as the thread will be waiting on the download to finish.

Let’s see this in code. We will create a fake web service which will wait for 5 seconds (pretending to download something) and then will send a response back to the caller.

Thread.sleep, sleeps the current thread for the given time interval, so no run loop processing occurs while the thread is blocked or sleeping.

class WebService {
    func getDataFromServer() -> String {
        Thread.sleep(until: Date().addingTimeInterval(5))
        return "Here are the results"
    }
}

Let’s use this service inside a SwiftUI view.

struct DevTechieAsyncAwaitExample: View {
    
    @State private var counter = 0
    @State private var response = ""
    
    var body: some View {
        NavigationStack {
            VStack {
                Text("Server said: \(response)")
                Text("Tap counter: **\(counter)**")
                
                Button("Tap me while waiting") {
                    counter += 1
                }
                .buttonStyle(.borderedProminent)
                
                Button("Initiate task") {
                    response = WebService().getDataFromServer()
                }
                .buttonStyle(.borderedProminent)
            }
            .navigationTitle("DevTechie")
        }
    }
}

Build and run

Notice the counter button works until we tap on the initiate task button. Reason UI is unresponsive because we are doing work on the main thread and when we call Thread.Sleep on it, the entire thread is blocked.

In a production environment, we would not be calling Thread.sleep but would be waiting for some image or content to be downloaded, but again, we don’t want to block the main thread in that case either.

Let’s see this with another example where we will download an image from a remote API. We will change our service to download 4000x4000 image from picsum.photos API.

class WebService {
    func getDataFromServer() -> Image? {
        let imageData = try? Data(contentsOf: URL(string: "https://picsum.photos/4000")!)
        return Image(uiImage: UIImage(data: imageData!)!)
    }
}

Let’s also change the view to render the downloaded image.

struct DevTechieAsyncAwaitExample: View {
    
    @State private var counter = 0
    @State private var response: Image?
    
    var body: some View {
        NavigationStack {
            VStack {
                if let image = response {
                    image
                        .resizable()
                        .frame(width: 400, height: 400)
                }
                Text("Tap counter: **\(counter)**")
                
                Button("Tap me while waiting") {
                    counter += 1
                }
                .buttonStyle(.borderedProminent)
                
                Button("Initiate task") {
                    response = WebService().getDataFromServer()
                }
                .buttonStyle(.borderedProminent)
            }
            .navigationTitle("DevTechie")
        }
    }
}

Notice that tapping on the initiate task button, blocks the whole UI and app becomes unresponsive until the image is rendered on to the screen.

This time, even Xcode complains with a purple warning message

Here is how our app’s thread looks like while everything is being worked on the main thread.

In the past, we moved computationally heavy work in the background using GCD (which was originally written for Objective-C and was ported over to Swift). Which is still relevant, and in use, but since Swift 5.5 release, we have a Swifty way of handling the same work.

With async and await, we want to move the image download task to a background thread and when the image is downloaded, render it on main thread.

We will turn our image downloading function into an asynchronous function. Note that we are also throwing any error the service may encounter to catch it at the caller level.

class WebService {
    func getDataFromServer() async throws -> Image? {
        let imageData = try Data(contentsOf: URL(string: "https://picsum.photos/4000")!)
        return Image(uiImage: UIImage(data: imageData)!)
    }
}

Since the service is asynchronous, we can’t really call it directly from the button action as we were doing it before. We will get a compiler error.

This can be easily fixed by wrapping the call to service inside a Task structure which is designed to handle asynchronous work.

Task {
    response = try? await WebService().getDataFromServer()
}

The complete view code should look like this:

struct DevTechieAsyncAwaitExample: View {
    
    @State private var counter = 0
    @State private var response: Image?
    
    var body: some View {
        NavigationStack {
            VStack {
                if let image = response {
                    image
                        .resizable()
                        .frame(width: 400, height: 400)
                }
                Text("Tap counter: **\(counter)**")
                
                Button("Tap me while waiting") {
                    counter += 1
                }
                .buttonStyle(.borderedProminent)
                
                Button("Initiate task") {
                    Task {
                        response = try? await WebService().getDataFromServer()
                    }
                }
                .buttonStyle(.borderedProminent)
            }
            .navigationTitle("DevTechie")
        }
    }
}

Build and run.

Notice that we never stopped tapping on the button and the counter kept going up without any interruptions.