Reading time: 1 min

This quick recipe shows how to implement a Combine map operator that takes an async block. To do this, you need to:

  1. Use Task to run async code in a sync context.
  2. Use the Future publisher to publish the new value the Task is completed.
  3. Use flatMap to transfer from your current publisher to the Future one.

Here's the code for two async map operators - once that takes a throwing block and one that doesn't. The names are chosen to make it very clear what each method does:

extension Publisher {
  func mapAsyncThrows<T>(
    _ transform: @escaping (Output) async throws -> T
  ) -> Publishers.FlatMap<Future<T, Error>, Publishers.SetFailureType<Self, Error>> {
    flatMap { value in
      Future { promise in
        Task {
          do {
            let output = try await transform(value)
            promise(.success(output))
          } catch {
            promise(.failure(error))
          }
        }
      }
    }
  }

  func mapAsync<T>(
      _ transform: @escaping (Output) async -> T
  ) -> Publishers.FlatMap<Future<T, Never>, Self> {
    flatMap { value in
      Future { promise in
        Task {
          promise(.success(await transform(value)))
        }
      }
    }
  }
}

And here's some sample code that downloads images from a cool site as their URLs are published:

["https://swiftuirecipes.com/user/pages/01.blog/swiftui-multiple-buttons-in-list-row/preview.png",
 "https://swiftuirecipes.com/user/pages/01.blog/swiftui-system-images-icons-sf-symbols-cheatsheet-size-color-variant/sizing.png",
 "https://swiftuirecipes.com/user/pages/01.blog/swiftui-system-images-icons-sf-symbols-cheatsheet-size-color-variant/coloring.png"
]
  .publisher
  .mapAsyncThrows { string -> UIImage? in
    guard let url = URL(string: string)
    else {
      throw URLError(.badURL)
    }
    let (data, _) = try await URLSession.shared.data(from: url)
    return UIImage(data: data)
  }

Next Post Previous Post