Swift Async let with loop

Viewed 1946

I want to get data in parallel. I found an example to call API in parallel but I want to store async let variables with loop.

Async let example. However, this example doesn't use a loop.

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]
show(photos)

I want to do something like the following.

let items = photoNames.map({ photo in
    async let item = downloadPhoto(named: photo)
    return item
}) 
let photos = await items
show(photos)
3 Answers

You can use a task group. See Tasks and Task Groups section of the The Swift Programming Language: Concurrency (which would appear to be where you got your example).

One can use withTaskGroup(of:returning:body:) to create a task group to run tasks in parallel, but then collate all the results together at the end.

E.g. here is an example that creates child tasks that return a tuple of “name” and ”image”, and the group returns a combined dictionary of those name strings with their associated image values:

func downloadImages(names: [String]) async -> [String: UIImage] {
    await withTaskGroup(
        of: (String, UIImage).self,
        returning: [String: UIImage].self
    ) { [self] group in
        for name in names {
            group.addTask { await (name, downloadPhoto(named: name)) }
        }

        var images: [String: UIImage] = [:]

        for await result in group {
            images[result.0] = result.1
        }

        return images
    }
}

Or, more concisely:

func downloadImages(names: [String]) async -> [String: UIImage] {
    await withTaskGroup(of: (String, UIImage).self) { [self] group in
        for name in names {
            group.addTask { await (name, downloadPhoto(named: name)) }
        }

        return await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
    }
}

They run in parallel:

enter image description here

But you can extract them from the dictionary of results:

let stooges = ["moe", "larry", "curly"]
let images = await downloadImages(names: stooges)

imageView1.image = images["moe"]
imageView2.image = images["larry"]
imageView3.image = images["curly"]

Or if you want an array sorted in the original order, just build an array from the dictionary:

func downloadImages(names: [String]) async -> [UIImage] {
    await withTaskGroup(of: (String, UIImage).self) { [self] group in
        for name in names {
            group.addTask { await (name, downloadPhoto(named: name)) }
        }

        let dictionary = await group.reduce(into: [:]) { $0[$1.0] = $1.1 }
        return names.compactMap { dictionary[$0] }
    }
}

Rob's answer is good. You can use an Array instead of a Dictionary too, to preserve order.

let photos = await photoNames.map(downloadPhoto)
public extension Sequence {  
  func map<Transformed>(
    priority: TaskPriority? = nil,
    _ transform: @escaping (Element) async throws -> Transformed
  ) async rethrows -> [Transformed] {
    try await withThrowingTaskGroup(
      of: EnumeratedSequence<[Transformed]>.Element.self
    ) { group in
      for (offset, element) in enumerated() {
        group.addTask(priority: priority) {
          (offset, try await transform(element))
        }
      }
      
      return try await group.reduce(
        into: map { _ in nil } as [Transformed?]
      ) {
        $0[$1.offset] = $1.element
      } as! [Transformed]
    }
  }
}

If the order of result doesn't matter here, use a TaskGroup instead.

Related