0

I am working on a use case where the first API call will fetch a list of metadata elements and based on the output, the next API call will parallelly fetch contents for each metadata. Finally, I need to collect all the output and return an array of contents ([Content]) by updating some values from metadata.

  • If the first call fails, then it shouldn't proceed to the next call
  • data fetching can fail in 2nd API call for any metadata and if all fails it has to return a failure/error.
struct MetaData:  Codable {
    var identifier: String?
    var type: String?
    var url: String?
}

struct Content: Codable {
    var contentId: String? // this is from metaData
    var type: String? // this is from metaData
    var title: String?
    var subTitle: String?
    var subscriptionValue: String?
}

I am very new to Combine and still on the learning curve.

I created two methods

func fetchMetaData() -> AnyPublisher<[MetaData], Error> {
    return Future<[MetaData], Error> { promise in
        //network call
        //if success {
            //promise(.success([MetaData]))
        //} else {
            //promise(.failure(Error()))
        //}
    }.eraseToAnyPublisher()
}

func fetchContents(metaData: MetaData) -> AnyPublisher<Content, Error> {
    return Future<Content, Error> { promise in
        //network call
        //if success {
            //promise(.success(Content))
        //} else {
            //promise(.failure(Error()))
        //}
    }.eraseToAnyPublisher()
}

How can I nest these functions and get the updated contents with type and identifier from metadata and handle failure scenarios?

ReB
  • 107
  • 10
  • Does this answer your question? [Combine perform nested requests](https://stackoverflow.com/questions/69164401/combine-perform-nested-requests) – timbre timbre Feb 22 '23 at 22:46
  • @rapiddevice, actually not. This thread is talking about making only one call. But my problem is having a list. so it has to loop through the list and make parallel calls. – ReB Feb 22 '23 at 22:57
  • As currently stated, your question is about nested network calls, and the approach in the linked question (using `flatMap` etc) describes a basic approach applicable to a variety of the use cases, including yours. Nothing in your question was about "parallel calls", so maybe make your question more clear to avoid marking it as duplicate. – timbre timbre Feb 22 '23 at 23:02
  • It's still quite unclear what the goal is. What's the part you don't know? Is it, how to perform an unknown number of tasks in parallel? – matt Feb 22 '23 at 23:43
  • @matt, the goal is to get an array of contents from nested API calls. for the list of metadata, I want to execute a parallel network call to fetch its content and collect all the results and assign them to an array. – ReB Feb 22 '23 at 23:48
  • `I am very new to Combine and still on the learning curve.`, in that case why not try a more modern way designed specifically for dealing with concurrency and parallel processing, using: [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/) – workingdog support Ukraine Feb 22 '23 at 23:53
  • Sorry, that's all just gibberish to me. I know how to execute publishers in parallel for an array: publish the array and use flatmap. I know how to collect the results into an array: use collect. I even know how to keep the original array elements paired with their corresponding results, if that would be helpful to you. But I am afraid I can't say more without a less jargonistic more concrete problem statement. – matt Feb 22 '23 at 23:53
  • In this article I show three different ways of solving what I _think_ your problem is: https://www.biteinteractive.com/swift-5-5-asynchronous-looping-with-async-await/ But really I have no idea if it is what you're looking for. – matt Feb 22 '23 at 23:56
  • @matt sure will have a look at this. I am also referring to this question asked by you https://stackoverflow.com/questions/61841254/combine-framework-how-to-process-each-element-of-array-asynchronously-before-pr?noredirect=1&lq=1 – ReB Feb 23 '23 at 00:34
  • Well found, but my understanding of your question is that you want the opposite what I discuss in that question. You want parallel tasks whereas that question is about serial tasks. – matt Feb 23 '23 at 00:53

1 Answers1

0

First, The fetchContents() needs to accept the metaData object that it is fetching the contents for. So I'm updating the function's type... Second, I'm assuming you want a single array of Contents from all metadata objects. If any one fetchContents call fails, then just don't show the contents for that particular metadata...

The special challenge here is that the error should only emit if all the secondary fetches fail. That was a bit of a chore...

Here's what I ended up with.

func fetchMetaData() -> AnyPublisher<[MetaData], Error> { fatalError() }
func fetchContents(metaData: MetaData) -> AnyPublisher<[Content], Error> { fatalError() }

struct AllErrors: Error { }

func example() -> AnyPublisher<[Content], Error> {
    fetchMetaData()
        .flatMap { metaData -> AnyPublisher<[Result<[Content], Error>], Never> in
            // set up the secondary calls. make sure to capture the errors so they don't propagate.
            let contents = metaData.map {
                fetchContents(metaData: $0)
                    .map { Result<[Content], Error>.success($0) }
                    .catch { Just(Result<[Content], Error>.failure($0)) }
            }

            // combine all the publishers into a single publisher.
            return combineLatest(contents)
        }
        .tryMap {
            // if there is no metaData, then just emit an empty array of contents
            guard !$0.isEmpty else { return [] }
            // if all fetchContents failed, then throw the error
            guard !$0.allSatisfy({ (try? $0.get()) == nil }) else { throw AllErrors() }
            // flatten the contents arrays.
            return $0.flatMap { (try? $0.get()) ?? [] }
        }
        .eraseToAnyPublisher()
}

// this is a handy generic function for combining an array of publishers into a single publisher of an array type.
func combineLatest<A, Failure: Error>(_ xs: [AnyPublisher<A, Failure>]) -> AnyPublisher<[A], Failure> {
    xs.reduce(Empty<[A], Failure>().eraseToAnyPublisher()) { state, x in
        state.combineLatest(x)
            .map { $0.0 + [$0.1] }
            .eraseToAnyPublisher()
    }
}
Daniel T.
  • 32,821
  • 6
  • 50
  • 72