Search code examples
swiftcombine

Chaining with flatMap


I'm having difficulty trying to figure out how to loop TMDb results from my publisher 1 and use the value I'm getting from key id for my second publisher. It's not looping through each result, it does get the proper URL inside my func fetchVideos(_ id: Int) but it's not calling each URL from TMDb's results array. I'm not sure if it's because I'm also getting an Array of results from Videos Codable data?

I attempted to use Publishers.MergeMany in my first publisher's flatMap. I'm still definitely at a novice level for combine, any tips would help. I'm trying to get a list of movies, then from the movies get the id key then use that to fetch the movie trailer data for each movie.

print output

https://api.themoviedb.org/3/movie/602223/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/459151/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/385128/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/522478/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/637693/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/529203/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/578701/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/631843/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/645856/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/581644/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/436969/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/568620/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/522931/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/681260/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/630586/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/671/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/618416/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/646207/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/550988/videos?api_key=API_KEY&language=en-US
https://api.themoviedb.org/3/movie/482373/videos?api_key=API_KEY&language=en-US


Videos(id: 459151, results: [themoviedb_demo.Video(id: 3F24D610-4261-4AEB-8906-A9D0E5FE8E4D, iso639_1: "en", iso3166_1: "US", key: "CK6xdYIsaa0", name: "DreamWorks\' The Boss Baby: Family Business | Official Trailer #3 | Peacock", site: "YouTube", size: 1080, type: "Trailer"), themoviedb_demo.Video(id: 417EF49C-B983-4CC3-B435-7A902DECE917, iso639_1: "en", iso3166_1: "US", key: "-rF2j6K5FoM", name: "The Boss Baby 2: Family Business – Official Trailer 2 (Universal Pictures) HD", site: "YouTube", size: 1080, type: "Trailer"), themoviedb_demo.Video(id: C34A3F5F-9429-4267-86F0-5506EF3E8281, iso639_1: "en", iso3166_1: "US", key: "QPzy8Ckza08", name: "THE BOSS BABY: FAMILY BUSINESS | Official Trailer", site: "YouTube", size: 1080, type: "Trailer")])

Codable data

struct Videos: Codable {
    let id: Int
    let results: [Video]
}

struct Video: Codable {
    let id = Int
    let key: String
    let name: String

    enum CodingKeys: String, CodingKey {
        case id
        case key, name
    }
}


struct TMDb: Codable {
    let results: [Results]?
}

struct Results: Codable {
    let id: Int
    let releaseDate, title: String?
    let name: String?

    enum CodingKeys: String, CodingKey {
        case id
        case releaseDate = "release_date"
        case title
        case name
    }
}

@Published var movies = TMDb(results: Array(repeating: Results(id: 1, releaseDate: "", title: "", name: "") , count: 5))
@Published var videos = Videos(id: 1, results: Array(repeating: Video(id: 1, key: "", name: "") , count: 5))

func getUpcoming() {
    var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
    request.httpMethod = "GET"
    let publisher = URLSession.shared.dataTaskPublisher(for: request)
        .map{ $0.data }
        .decode(type: TMDb.self, decoder: JSONDecoder())
    let publisher2 = publisher
        .flatMap{
            // loop results from TMDb for id for publisher 2, only one is called
            Publishers.MergeMany($0.results!.map { item in
                return self.fetchVideos(item.id)
                    .map { $0 as Videos }
                    .replaceError(with: nil)
            })
        }
    // Publishers.CombineLatest
    Publishers.Zip(publisher, publisher2)
        .receive(on:  DispatchQueue.main)
        .sink(receiveCompletion: {_ in
        }, receiveValue: { movies, videos in
            self.movies = movies
            self.videos = videos
        }).store(in: &cancellables)
}

func fetchVideos(_ id: Int) -> AnyPublisher<Videos, Error> {
    let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/videos?api_key=API_KEY&language=en-US")!
    return URLSession.shared.dataTaskPublisher(for: url)
        .mapError { $0 as Error }
        .map{ $0.data }
        .decode(type: Videos.self, decoder: JSONDecoder())
        .eraseToAnyPublisher()
}

Solution

  • Hi @cole for this operation you don't need either the merge or the zip, because you are not subscribing to two publishers, you are attempting to do an action after your first publisher emitted an event.

    For this you only need a map .handleEvents in my opinion.

    So lets try to enhance your code, we want to update both movies and videos separately, but we still need videos to be dependent of movies

    First we will create the publisher to request the movies:

    var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
    request.httpMethod = "GET"
    let publisher = URLSession.shared.dataTaskPublisher(for: request)
        .map{ $0.data }
        .decode(type: TMDb.self, decoder: JSONDecoder())
    

    Now we enhance this publisher handling by assigning movies:

    var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
    request.httpMethod = "GET"
    let publisher = URLSession.shared.dataTaskPublisher(for: request)
        .map{ $0.data }
        .decode(type: TMDb.self, decoder: JSONDecoder())
        .sink(receiveCompletion: { print ($0) },
          receiveValue: { self.movies = $0.results })
    

    Now we will add .handleEvent in order to iterate through our movies to create all the publishers which emit videos events and append videos for the videos array:

    var request = URLRequest(url:URL(string:"https://api.themoviedb.org/3/movie/upcoming?api_key=API_KEY&language=en-US&page=1")!)
    request.httpMethod = "GET"
    let publisher = URLSession.shared.dataTaskPublisher(for: request)
        .map{ $0.data }
        .decode(type: TMDb.self, decoder: JSONDecoder())
        .sink(receiveCompletion: { print ($0) },
          receiveValue: { self.movies = $0.results })
        .handleEvents(receiveSubscription:nil, receiveOutput: { [weak self] movies in guard let self = self else {return} 
        self.videos = [Videos]()
        for movie in movies.results {
              self.fetchVideos(movie.id)
        }, receiveCompletion:nil, receiveCancel:nil, receiveRequest:nil)
    })
         .store(in: &cancellables)
    

    Now for the last step lets update the fetchVideos accordingly:

    func fetchVideos(_ id: Int) {
    let url = URL(string: "https://api.themoviedb.org/3/movie/\(id)/videos?api_key=API_KEY&language=en-US")!
    return URLSession.shared.dataTaskPublisher(for: url)
        .mapError { $0 as Error }
        .map{ $0.data }
        .decode(type: Videos.self, decoder: JSONDecoder())
        .sink(receiveCompletion: { print ($0) },
          receiveValue: { [weak self] videos in guard let self = self else {return}
             self.videos.append(videos)
      })
        .store(in: &cancellables)
     }