How to await an arbitrary number of async functions in parallel

Viewed 320

The Swift Concurrency documentation provides the following example for how to await multiple async functions in parallel.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]

Is there a way to await an arbitrary number of async functions in parallel? I tried the approach shown in the code below, but could not find a way to make it work.

var photoPromises: [PhotoPromise] = []
for name in photoNames {
    photoPromises.append(downloadPhoto(named: name))
}
let photos = await photoPromises
2 Answers
import UIKit
import XCTest

struct Downloader
{
    static func download(photoURLs: [URL]) async throws -> [UIImage] {
        try await withThrowingTaskGroup(of: UIImage.self) { group in
            var collected = [UIImage]()
            for url in photoURLs {
                group.addTask {
                    let (data, _) = try await URLSession.shared.data(from: url)
                    return UIImage(data: data)!
                }
            }
            for try await value in group {
                collected.append(value)
            }
            return collected
        }
    }
}

final class PhotosTests: XCTestCase
{
    func testDownloader() async throws {
        let images = try await Downloader.download(photoURLs: [
            URL(string: "https://placekitten.com/200/300")!,
            URL(string: "https://placekitten.com/g/200/300")!
        ])
        XCTAssertNotNil(images[0])
        XCTAssertNotNil(images[1])
        XCTAssertEqual(images.count, 2)
    }
}

Generic map/reduce version

import UIKit
import XCTest

struct MapReduce
{
    static func mapReduce<T,U>(inputs: [T], process: @escaping (T) async throws -> U ) async throws -> [U] {
        try await withThrowingTaskGroup(of: U.self) { group in
            // map
            for input in inputs {
                group.addTask {
                    try await process(input)
                }
            }
            // reduce
            var collected = [U]()
            for try await value in group {
                collected.append(value)
            }
            return collected
        }
    }
}

final class PhotosTests: XCTestCase
{
    func testDownloader() async throws {
        let input = [
            URL(string: "https://placekitten.com/200/300")!,
            URL(string: "https://placekitten.com/g/200/300")!
        ]
        let download: (URL) async throws -> UIImage = { url in
            let (data, _) = try await URLSession.shared.data(from: url)
            return UIImage(data: data)!
        }
        let images = try await MapReduce.mapReduce(inputs: input, process: download)
        XCTAssertNotNil(images[0])
        XCTAssertNotNil(images[1])
        XCTAssertEqual(images.count, 2)
    }
}

Yes, you have the right idea. I found a similar question here:

Try using a when statement instead to await for fulfilled promises:


when(fulfilled: promiseArray).then { results in
    // Do something
}.catch { error in
    // Handle error
}

Here is the original question but the link referenced there is broken.

Swift & PromiseKit: Resolving ALL promises from a loop

Related