12

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

4
  • 1
    Your combine code doesn't necessarily fetch the images in the same order as the people array. Is that intentional? Commented Feb 11, 2022 at 16:41
  • 1
    I'm sure you've seen the WWDC video but it looks like you can just yield in the sink of the method you already have. Commented Feb 11, 2022 at 16:43
  • 2
    John Sundell wrote a bunch of helpful articles about Swift Concurrency among others also to migrate existing APIs to async/await. Commented Feb 11, 2022 at 16:46
  • @Sweeper ah, I didn’t know that but in the actual code the order of the data doesn’t matter. Thanks though, I’ll keep that in mind in the future. 👍🏻 Commented Feb 11, 2022 at 17:52

1 Answer 1

17

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: [:]) { 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? { … }
}
Sign up to request clarification or add additional context in comments.

1 Comment

FWIW, there is a bug in Swift 5.10 that will give a warning at the reduce line. This is a temporary state of affairs, to be fixed in future Swift version. For now, you would for await (id, avatar) in group {…} and manually accumulate the results in a dictionary.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.