3

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
Jano
  • 62,815
  • 21
  • 164
  • 192
pdiffley
  • 603
  • 9
  • 18

2 Answers2

2
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)
    }
}
Jano
  • 62,815
  • 21
  • 164
  • 192
-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

Manuel Duarte
  • 644
  • 4
  • 18
  • Unfortunately, this solution only applies to PromiseKit promises. The question at hand is for async functions called with the newly introduced Swift Concurrency. – pdiffley Jan 06 '22 at 06:12