iilyasov
iilyasov

Reputation: 23

How to merge multiple network calls’ responses into an array? With or without Combine?

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

And also their Combine variations

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:

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

Answers (1)

Daniel T.
Daniel T.

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

Related Questions