Home > Blockchain >  Swift map from array of objects to array of async functions and await them all
Swift map from array of objects to array of async functions and await them all

Time:02-14

I've just updated to Xcode 13.2.1 and now have access to async await and I'm trying to find places I can "convert" from Combine over to async await.

I want to achieve the following...

Given a type like...

struct Person {
  let name: String

  func fetchAvatar(completion: @escaping (UIImage?) -> Void) {
    // fetch the image from the web and pass it into the completion.
  }
}

I currently have a function like this...

func fetchAllTheAvatars(people: [Person], completion: ([UIImage]) -> Void) {
  Publisher.MergeMany(
    people.map { person in
      Future<UIImage?, Never> { promise in
        person.fetchAvatar { promise(.success($0)) }
      }
    }
  )
  .compactMap { $0 }
  .collect()
  .sink { completion($0) }
  .store(in: &cancellables )
}

Now... it looks to me like this could be a good candidate for moving to using async await and AsyncSequence maybe...?!? It doesn't have to be ideal though, I just want to get a feel of how to use them. I'm used to async await in JS and TS and this just seems a little bit different. :D

I added a wrapper function to my Person...

func fetchAvatar() async -> UIImage? {
  await withCheckedContinuation { continuation in
    fetchAvatar { image in
      continuation.resume(returning: image)
    }
  }
}

But now I'm stuck on how to update my fetchAllTheAvatars function.

func fetchAllTheAvatars(people: [Person]) async -> [UIImage] {
  people.map { ...???... }
}

Everywhere I have seen online seems to use for await line in url.lines { ... } but I don't yet have an AsyncSequence. I need to somehow "convert" my non-async array of Person into an AsyncSequence of () -> Image?.

Is that possible? Am I going about this entirely the wrong way?

Thanks

CodePudding user response:

The standard pattern is TaskGroup. Add your tasks for the individual images, and then await in a for loop, map, or, in this case, reduce:

func fetchAllTheAvatars(people: [Person]) async -> [Person.ID: UIImage] {
    await withTaskGroup(of: (Person.ID, UIImage?).self) { group in
        for person in people {
            group.addTask { await (person.id, person.fetchAvatar()) }
        }
        
        return await group.reduce(into: [Person.ID: UIImage]()) { (dictionary, result) in 
            if let image = result.1 {
                dictionary[result.0] = image
            }
        }
    }
}

Note, because the order is not guaranteed and because some of your Person may not return an image, my implementation returns an efficient, order-independent, structure (i.e., a dictionary).

Needless to say, the above assumes that you make Person conform to Identifiable:

struct Person: Identifiable {
    let id = UUID()
    let name: String
    
    func fetchAvatar() async -> UIImage? { … }
}
  • Related