3

I'm trying to compose a nested publisher chain in combine with Swift and I'm stumped. My current code starts throwing errors at the .flatMap line, and I don't know why. I've been trying to get it functional but am having no luck.

What I'm trying to accomplish is to download a TrailerVideoResult and decode it, grab the array of TrailerVideo objects, transform that into an array of YouTube urls, and then for each YouTube URL get the LPLinkMetadata. The final publisher should return an array of LPLinkMetadata objects. Everything works correctly up until the LPLinkMetadata part.

EDIT: I have updated the loadTrailerLinks function. I originally forgot to remove some apart of it that was not relevant to this example.

You will need to import "LinkPresentation". This is an Apple framework for to fetch, provide, and present rich links in your app.

The error "Type of expression is ambiguous without more context" occurs at the very last line (eraseToAnyPublisher).

func loadTrailerLinks() -> AnyPublisher<[LPLinkMetadata], Error>{    
    return URLSession.shared.dataTaskPublisher(for: URL(string: "Doesn't matter")!)
        .tryMap() { element -> Data in
            guard let httpResponse = element.response as? HTTPURLResponse,
                  httpResponse.statusCode == 200 else {
                throw URLError(.badServerResponse)
            }
            return element.data
        }
        .decode(type: TrailerVideoResult.self, decoder: JSONDecoder(.convertFromSnakeCase))
        .compactMap{ $0.results }
        .map{ trailerVideoArray -> [TrailerVideo] in
            let youTubeTrailer = trailerVideoArray.filter({$0.site == "YouTube"})
            return youTubeTrailer
        }
        .map({ youTubeTrailer -> [URL] in
            return youTubeTrailer.compactMap{
                let urlString = "https://www.youtube.com/watch?v=\($0.key)"
                let url = URL(string: urlString)!
                return url
            }
        })
        .flatMap{ urls -> [AnyPublisher<LPLinkMetadata, Never>] in
            return urls.map{ url -> AnyPublisher <LPLinkMetadata, Never> in
                return self.getMetaData(url: url)
                    .map{ metadata -> LPLinkMetadata in
                        return metadata
                    }
                    .eraseToAnyPublisher()
            }
        }
        .eraseToAnyPublisher()
}
func fetchMetaData(url: URL) -> AnyPublisher <LPLinkMetadata, Never> {
    return Deferred {
        Future { promise in
            LPMetadataProvider().startFetchingMetadata(for: url) { (metadata, error) in
                promise(Result.success(metadata!))
            }
        }
    }.eraseToAnyPublisher()
}
struct TrailerVideoResult: Codable {
    let results : [TrailerVideo]
}
struct TrailerVideo: Codable {
    let key: String
    let site: String
}
Richard Witherspoon
  • 4,082
  • 3
  • 17
  • 33
  • I'm getting the compile error "Type of expression is ambiguous without more context" – Richard Witherspoon Nov 01 '20 at 02:20
  • Where are you getting the error? Please edit your question to include the error information. – Lauren Yim Nov 01 '20 at 02:23
  • 2
    `.flatMap` expects a `Publisher` as a return value in its closure, but you're returning an array (of publishers, sure, but it's not a publisher on its own) – New Dev Nov 01 '20 at 02:24
  • Why are you using `compactMap` in `.compactMap { $0.results }` and `youTubeTrailer.compactMap { /* ... */ }`? In the first one, `results` cannot be `nil`. In the second, you're force unwrapping the URL, so `nil` will never be returned. – Lauren Yim Nov 01 '20 at 02:31
  • I have update the question. Sorry about that. – Richard Witherspoon Nov 01 '20 at 02:33
  • @NewDev Oh, I thought that because it was an array of publishers, that a flatMap was needed. – Richard Witherspoon Nov 01 '20 at 02:36
  • @cherryblossom you're right, those can be done with a regular .map. I also shouldn't be force unwrapping the url, but I can fix that later. I'm just trying to get the correct output for now; thats the part thats really giving me trouble. – Richard Witherspoon Nov 01 '20 at 02:38
  • Combine's `.flatMap` "flattens" publishers into values (sort of a loose parallel to `.flatMap` of a `Sequence`), so you'd still need it. But more work would be need to be done to collect the values into an array – New Dev Nov 01 '20 at 02:43
  • @NewDev would it be possible for you to provide a solution? That "extra work" is what is alluding me. – Richard Witherspoon Nov 01 '20 at 02:47

2 Answers2

3

You can use Publishers.MergeMany and collect() for this:

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

func loadTrailerLinks() -> AnyPublisher<[LPLinkMetadata], Error> {
  // Download data
  URLSession.shared.dataTaskPublisher(for: URL(string: "Doesn't matter")!)
    .tryMap() { element -> Data in
      guard let httpResponse = element.response as? HTTPURLResponse,
            httpResponse.statusCode == 200 else {
        throw URLError(.badServerResponse)
      }
      return element.data
    }
    .decode(type: TrailerVideoResult.self, decoder: decoder)
    // Convert the TrailerVideoResult to a MergeMany publisher, which merges the
    // [AnyPublisher<LPLinkMetadata, Never>] into a single publisher with output
    // type LPLinkMetadata
    .flatMap {
      Publishers.MergeMany(
        $0.results
          .filter { $0.site == "YouTube" }
          .compactMap { URL(string: "https://www.youtube.com/watch?v=\($0.key)") }
          .map(fetchMetaData)
      )
      // Change the error type from Never to Error
      .setFailureType(to: Error.self)
    }
    // Collect all the LPLinkMetadata and then publish a single result of
    // [LPLinkMetadata]
    .collect()
    .eraseToAnyPublisher()
}
Lauren Yim
  • 12,700
  • 2
  • 32
  • 59
1

It's a bit tricky to convert an input array of values to array of results, each obtained through a publisher.

If the order isn't important, you can flatMap the input into a Publishers.Sequence publisher, then deal with each value, then .collect them:

.flatMap { urls in 
    urls.publisher // returns a Publishers.Sequence<URL, Never> publisher
}
.flatMap { url in
    self.getMetaData(url: url) // gets metadata publisher per for each url
}
.collect()

(I'm making an assumption that getMetaData returns AnyPublisher<LPLinkMetadata, Never>)

.collect will collect all the emitted values until the upstream completes (but each value might arrive not in the original order)


If you need to keep the order, there's more work. You'd probably need to send the original index, then sort it later.

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • For me order doesn't matter and this works perfectly as well. Another good answer! – Richard Witherspoon Nov 01 '20 at 03:14
  • 1
    Well, if that's the answer, then this is a duplicate of https://stackoverflow.com/questions/61841254/combine-framework-how-to-process-each-element-of-array-asynchronously-before-pr where we hashed this all out already. – matt Nov 01 '20 at 03:39