Reputation: 23
Working with flagpedia.net/download/api
It has two endpoints:
1 - Returns [String:String] dictionary of code-country pairs, like [“us”:”United States”] as a json
2 - Returns an image data for a country code and specified image size, example url flagcdn.com/16x12/us.png
I have create two functions using regular completion handler
fetchCodes(completion: @escaping (Result<[String],Error>) -> Void) { … }
fetchImage(forCode: String, completion: @escaping (Result<UIImage,Error>) -> Void) { … }
And also their Combine variations
fetchCodes() -> AnyPublisher<[String],Error> { … }
fetchImage(forCode: String) -> AnyPublisher<UIImage,Error> { … }
Both methods works fine, and returns expected result. How could we merge them?
fetchCodes() method decodes json into dictionary and create an array from the keys.
After getting codes: [String] from fetchCodes() want to do something like this:
var results = [UIImage]()
for code in codes {
let image = fetchImage(forCode: code)
images.append(image)
}
How to achieve this?
I tried Publishers.ManyMerge and flatMap but wasn’t successful. Ended up getting warning regarding mismatch in return types.
Sorry for the indentation, I’m posting this on mobile.
Upvotes: 0
Views: 810
Reputation: 33967
Here's a great way to do it with the Combine operators:
func fetchCodes() -> AnyPublisher<[String],Error> { fatalError() }
func fetchImage(forCode: String) -> AnyPublisher<UIImage,Error> { fatalError() }
func example() -> AnyPublisher<[String: UIImage], Error> {
let codesAndImages = keysAndValues(fetchImage(forCode:))
return fetchCodes()
.flatMap { codes in
combineLatest(codesAndImages(codes))
}
.map(Dictionary.init(uniqueKeysWithValues:))
.eraseToAnyPublisher()
}
func keysAndValues<A, B>(_ get: @escaping (A) -> AnyPublisher<B, Error>) -> ([A]) -> [AnyPublisher<(A, B), Error>] {
{ xs in
xs.map { x in
Just(x).setFailureType(to: Error.self)
.zip(get(x))
.eraseToAnyPublisher()
}
}
}
func combineLatest<A>(_ xs: [AnyPublisher<A, Error>]) -> AnyPublisher<[A], Error> {
xs.reduce(Empty<[A], Error>().eraseToAnyPublisher()) { state, x in
state.combineLatest(x)
.map { $0.0 + [$0.1] }
.eraseToAnyPublisher()
}
}
or you could do it like this which is flatter:
func example() -> AnyPublisher<[String: UIImage], Error> {
fetchCodes()
.map { codes in
codes.map { Just($0).setFailureType(to: Error.self).zip(fetchImage(forCode: $0)) }
}
.flatMap { zip($0) }
.map(Dictionary.init(uniqueKeysWithValues:))
.eraseToAnyPublisher()
}
func zip<Pub>(_ xs: [Pub]) -> AnyPublisher<[Pub.Output], Pub.Failure> where Pub: Publisher {
xs.reduce(Empty<[Pub.Output], Pub.Failure>().eraseToAnyPublisher()) { state, x in
state.zip(x)
.map { $0.0 + [$0.1] }
.eraseToAnyPublisher()
}
}
Without Combine it gets a bit more tricky:
func fetchCodes(completion: @escaping (Result<[String],Error>) -> Void) { }
func fetchImage(forCode: String, completion: @escaping (Result<UIImage,Error>) -> Void) { }
func example(_ completion: @escaping (Result<[String: UIImage], Error>) -> Void) {
fetchCodes { codesResult in
switch codesResult {
case let .success(codes):
loadImages(codes: codes, completion)
case let .failure(error):
completion(.failure(error))
}
}
}
func loadImages(codes: [String], _ completion: @escaping (Result<[String: UIImage], Error>) -> Void) {
var result = [String: UIImage]()
let lock = NSRecursiveLock()
let group = DispatchGroup()
for code in codes {
group.enter()
fetchImage(forCode: code) { imageResult in
switch imageResult {
case let .success(image):
lock.lock()
result[code] = image
lock.unlock()
group.leave()
case let .failure(error):
completion(.failure(error))
}
}
}
group.notify(queue: .main) {
completion(.success(result))
}
}
With the Combine version if a fetchImage fails, then the others are all canceled. With the callback version, that is not the case. Instead, the others will finish and then throw away the data downloaded.
Upvotes: 1