swiftcombine

Swift-Combine, connect all AnyPublisher to one and receive data


I want migrate to combine and now trying to understand how to do this. By learning i faced with one problem, that i don't understand how better to do.

I have dynamic lists with "AnyPublisher".

I will describe what i have for better understanding what i want.

I have methods for making requests, methods returns AnyPublisher with type and APIError.

/// return standarts
func fetchStandarts(by key: String) -> AnyPublisher<[A11yStandart], APIError>

/// return accessibilities
func fetchAccessibilities(by id: String) -> AnyPublisher<AccessibilitiesResult, APIError>

/// return images
func fetchImages(id: String) -> AnyPublisher<AccessibilityImagesResult, APIError>

i have screen, where i need call fetchStandarts, fetchAccessibilities, fetchImages together for displaying screen.

But, for fetchStandarts and fetchAccessibilities i need to do multi requests, for example i have

let standartsIDs = ["1", "2"]
let accessibilityIDs = ["3", "4"]

for this case i need call fetchStandarts 2 times and fetchAccessibilities 2 times.

What i did, firstly i generate all Publishers

        let standardsPublishers = ["1", "2"]
            .map({ key -> AnyPublisher<[A11yStandart], APIError> in
                a11yStandartsRepository.fetchStandarts(by: key)
            })
        
        let answersPublishers = ["3", "4"]
            .map { key -> AnyPublisher<AccessibilitiesResult, APIError> in
                accessibilitiesRepository.fetchAccessibilities(by: key)
            }
        
        let imagesPublisher = a11ImagesRepository.fetchImages(id: "1")

now its most difficult part for me, because i dont know how better "merge"(by merge i don't mean merge operator in combine) all of publishers.

I tried do like this:

Publishers.MergeMany(standardsPublishers)
            .zip(Publishers.MergeMany(answersPublishers))
            .zip(imagesPublisher)
            .collect()

and like this

 Publishers.Zip(
    Publishers.MergeMany(standardsPublishers),
    Publishers.MergeMany(answersPublishers)
 )
.zip(imagesPublisher)

but im filing that im doing wrong this. Can you please give me advice, how i need correct "merge"(by merge i don't mean merge operator in combine) all publishers in one and receive data


Solution

  • I think understand what you're asking, but if not, we can try again.

    Here is a Playground:

    import UIKit
    import Combine
    
    enum APIError : Error {
    }
    
    typealias DownloadedThing = String
    
    func makeRequest(downloading: String) -> Future<DownloadedThing, APIError> {
      Future { continuation in
        DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + .seconds((1...5).randomElement()!)) {
          print("Downloaded \(downloading)")
          continuation(.success("Downloaded \(downloading)"))
        }
      }
    }
    
    var thingsToDownload = ["Cats", "Dogs", "Elephants", "Ducks"]
    
    let subscription = thingsToDownload
      .publisher
      .flatMap(makeRequest)
      .sink { completion in
        switch completion {
        case .finished:
          print("All the requests are done")
        case .failure(let apiError):
          print("An API error caused a problem \(apiError)")
        }
      } receiveValue: { results in
        debugPrint(results)
      }
    

    In the playground, the list of things to be downloaded is in thingsToDownload. I also have a function, makeRequest(downloading:) that issues a request to download one thing. In this case it returns a Future. A Future is a Publisher that will emit one value or fail. If you want to, you can use .eraseToAnyPublisher() to erase the types, but I've left it as a Future so that my code communicates the fact that the request will either succeed or fail.

    Inside of makeRequest the Future that gets returned waits a random amount of time then succeeds. It simulates a network request that takes a few seconds to complete.

    The main thing you were asking about is in the pipeline at the bottom. I get the array and ask for its publisher so each of the values is passed as a value to the pipeline that follows.

    The important operator in the pipeline is flatMap. flatMap passes each of the values to my makeRequest(downloading:). As each publisher is returned, flatMap sets up a listener waiting for that publisher to complete before passing the result of the publisher down the pipeline.

    At the bottom is the sink call. As each of the publishers that are run by flatMap completes, this sink receives the resulting value and prints it to the console. If it were possible for my mock API calls to fail, then failures would also be delivered here.

    Running this playground on my system once yields:

    "Downloaded Ducks"
    "Downloaded Cats"
    "Downloaded Dogs"
    "Downloaded Elephants"
    All the requests are done
    

    and a second time gives:

    "Downloaded Ducks"
    "Downloaded Elephants"
    "Downloaded Cats"
    "Downloaded Dogs"
    All the requests are done
    

    So each result is delivered as it completes, and a message arrives when all the requests are done.