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

DevTechie Inc
Jul 21, 2023

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

In this part, we will learn to convert completion blocks from old APIs into newer async await versions.

Converting completion blocks into async await

Async and await were introduced in Swift 5.5 and before that, all of us were juggling between completion blocks, so it’s not a surprise that we have completion block code sitting in large quantities. With the new API calling completion block code inside async await, can be a bit challenging, so let’s try to build a solution for that too.

We will start by adding a completion block based API call to our web service.

class WebService {
    func getCoffeeList() async throws -> [Coffee] {
        let (data, _) = try await URLSession
            .shared
            .data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
        return try JSONDecoder().decode([Coffee].self, from: data)
    }
    
    func getCoffeeOldWay(completion: @escaping (Result<[Coffee], Error>) -> Void) {
        URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)) { data, response, error in
            
            guard error == nil else {
                completion(.failure(NSError(domain: "Error: \(error!.localizedDescription)", code: 232)))
                return
            }
            
            guard let data = data else {
                completion(.failure(NSError(domain: "No Data", code: 233)))
                return
            }
            
            do {
                let coffees = try JSONDecoder().decode([Coffee].self, from: data)
                completion(.success(coffees))
            } catch {
                completion(.failure(NSError(domain: "Error: \(error.localizedDescription)", code: 234)))
            }
        }
        .resume()
    }
}

We will update our SwiftUI view to use this new function instead of the async await version.

struct DevTechieAsyncAwaitExample: View {
    @State private var coffees = [Coffee]()
    
    var body: some View {
        NavigationStack {
            VStack {
                if coffees.isEmpty {
                    ZStack {
                        Image(systemName: "heater.vertical.fill")
                            .font(.system(size: 60))
                            .rotationEffect(.degrees(-90))
                            .offset(y: -20)
                            .foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
                        Image(systemName: "cup.and.saucer.fill")
                            .font(.system(size: 100))
                            .foregroundStyle(.gray.shadow(.inner(radius: 2)))
                    }
                    
                } else {
                    List(coffees) { coffee in
                        VStack(alignment: .leading, spacing: 5) {
                            Text(coffee.blendName)
                                .font(.title3)
                            Text("Notes: \(coffee.notes)")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                            HStack {
                                Text("Origin: \(coffee.origin)")
                                Spacer()
                                Text("Variety: \(coffee.variety)")
                            }
                            .font(.caption)
                            .foregroundStyle(.tertiary)
                        }
                    }
                }
            }
            .onAppear {
                WebService().getCoffeeOldWay { result in
                    switch result {
                    case .success(let responseCoffees):
                        coffees = responseCoffees
                        
                    case .failure(let err):
                        print(err.localizedDescription)
                    }
                }
            }
            .navigationTitle("DevTechie")
        }
    }
    
    private func refreshData() async {
        do {
            coffees = try await WebService().getCoffeeList()
        } catch {
            print(error.localizedDescription)
        }
    }
}

Build and run.

Now when we have a completion block based function, we can bring it into the modern world.

We will use the withCheckedThrowingContinuation function for this task. withCheckedThrowingContinuation was introduced in iOS 13 and it is used to suspend the current task and then it calls the passed callback with a CheckedContinuation object.

Inside the callback, we call the completion block based function, and when it finishes, we resume the execution of the task via the CheckedContinuation that withCheckedContinuation provided.

An important note here is that we must call continuation.resume() exactly once in the withCheckedContinuation block. If we forgot to do it, our app would be blocked forever. If we do it twice, the app will crash.

Let’s add another function to our service, which will call the completion block based method and encapsulate its response in an async await manner.

class WebService {
    func getCoffeeList() async throws -> [Coffee] {
        let (data, _) = try await URLSession
            .shared
            .data(from: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)
        return try JSONDecoder().decode([Coffee].self, from: data)
    }
    
    func getCoffeeOldWay(completion: @escaping (Result<[Coffee], Error>) -> Void) {
        URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=10")!)) { data, response, error in
            
            guard error == nil else {
                completion(.failure(NSError(domain: "Error: \(error!.localizedDescription)", code: 232)))
                return
            }
            
            guard let data = data else {
                completion(.failure(NSError(domain: "No Data", code: 233)))
                return
            }
            
            do {
                let coffees = try JSONDecoder().decode([Coffee].self, from: data)
                completion(.success(coffees))
            } catch {
                completion(.failure(NSError(domain: "Error: \(error.localizedDescription)", code: 234)))
            }
        }
        .resume()
    }
    
    func getCoffeeNewishWay() async throws -> [Coffee] {
        try await withCheckedThrowingContinuation({ continuation in
            getCoffeeOldWay { result in
                switch result {
                case .success(let coffeeResponse):
                    continuation.resume(returning: coffeeResponse)
                case .failure(let error):
                    continuation.resume(throwing: error)
                }
            }
        })
    }
}

Update the View with new code.

struct DevTechieAsyncAwaitExample: View {
    @State private var coffees = [Coffee]()
    
    var body: some View {
        NavigationStack {
            VStack {
                if coffees.isEmpty {
                    ZStack {
                        Image(systemName: "heater.vertical.fill")
                            .font(.system(size: 60))
                            .rotationEffect(.degrees(-90))
                            .offset(y: -20)
                            .foregroundStyle(.gray.opacity(0.4).shadow(.inner(radius: 2)))
                        Image(systemName: "cup.and.saucer.fill")
                            .font(.system(size: 100))
                            .foregroundStyle(.gray.shadow(.inner(radius: 2)))
                    }
                    
                } else {
                    List(coffees) { coffee in
                        VStack(alignment: .leading, spacing: 5) {
                            Text(coffee.blendName)
                                .font(.title3)
                            Text("Notes: \(coffee.notes)")
                                .font(.subheadline)
                                .foregroundStyle(.secondary)
                            HStack {
                                Text("Origin: \(coffee.origin)")
                                Spacer()
                                Text("Variety: \(coffee.variety)")
                            }
                            .font(.caption)
                            .foregroundStyle(.tertiary)
                        }
                    }
                }
            }
            .task {
                do {
                    coffees = try await WebService().getCoffeeNewishWay()
                } catch {
                    print(error)
                }
            }
            .navigationTitle("DevTechie")
        }
    }
}