Richard Witherspoon
Richard Witherspoon

Reputation: 5029

Swift Combine Nested Publishers

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
}

Upvotes: 3

Views: 5301

Answers (2)

New Dev
New Dev

Reputation: 49590

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.

Upvotes: 1

Lauren Yim
Lauren Yim

Reputation: 14088

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()
}

Upvotes: 3

Related Questions