06
May
2022
Combine Async Map
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:
- Use
Task
to run async code in a sync context. - Use the
Future
publisher to publish the new value theTask
is completed. - Use
flatMap
to transfer from your current publisher to theFuture
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)
}