Blackbeard
Blackbeard

Reputation: 662

Combine the results of 2 API calls fetching different properties for the same objects with RxSwift

I have a model called Track. It has a set of basic and a set of extended properties. List of tracks and their basic properties are fetched with a search API call, then I need to make another API call with those track IDs to fetch their extended properties.

The question is how to best combine the results of both API calls and populate the extended properties into the already created Track objects, and of course match them by ID (which unfortunately is a different property name in both calls' results). Note that there are many more keys returned in the real results sets - around 20-30 properties for each of the two calls.

Track.swift

struct Track: Decodable {

// MARK: - Basic properties

let id: Int
let title: String

// MARK: - Extended properties

let playbackURL: String

enum CodingKeys: String, CodingKey {
    case id = "id"

    case title = "title"

    case playbackUrl = "playbackUrl"
}

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    let idString = try container.decode(String.self, forKey: CodingKeys.id)
    id = idString.int ?? 0

    title = try container.decode(String.self, forKey: CodingKeys.title)

    playbackURL = try container.decodeIfPresent(String.self, forKey: CodingKeys.playbackUrl) ?? ""
}
}

ViewModel.swift

let disposeBag = DisposeBag()
var searchText = BehaviorRelay(value: "")
private let provider = MoyaProvider<MyAPI>()
let jsonResponseKeyPath = "results"

public lazy var data: Driver<[Track]> = getData()

private func searchTracks(query: String) -> Observable<[Track]> {
    let decoder = JSONDecoder()
    return provider.rx.request(.search(query: query))
        .filterSuccessfulStatusCodes()
        .map([Track].self, atKeyPath: jsonResponseKeyPath, using: decoder, failsOnEmptyData: false)
        .asObservable()
}

private func getTracksMetadata(tracks: Array<Track>) -> Observable<[Track]> {
    let trackIds: String = tracks.map( { $0.id.description } ).joined(separator: ",")
    let decoder = JSONDecoder()
    return provider.rx.request(.getTracksMetadata(trackIds: trackIds))
        .filterSuccessfulStatusCodes()
        .map({ result -> [Track] in

        })
        .asObservable()
}

private func getData() -> Driver<[Track]> {
    return self.searchText.asObservable()
        .throttle(0.3, scheduler: MainScheduler.instance)
        .distinctUntilChanged()
        .flatMapLatest(searchTracks)
        .flatMapLatest(getTracksMetadata)
        .asDriver(onErrorJustReturn: [])
}

The JSON result for .search API call is structured like this:

{
  "results": [
    {
      "id": "4912",
      "trackid": 4912,
      "artistid": 1,
      "title": "Hello babe",
      "artistname": "Some artist name",
      "albumtitle": "The Best Of 1990-2000",
      "duration": 113
    },
    { 
      ...
    }
  ]
}

The JSON result for .getTracksMetadata API call is structured like this:

[
  {
    "TrackID": "4912",
    "Title": "Hello babe",
    "Album": "The Best Of 1990-2000",
    "Artists": [
      {
        "ArtistID": "1",
        "ArtistName": "Some artist name"
      }
    ],
    "SomeOtherImportantMetadata1": "Something something 1",
    "SomeOtherImportantMetadata2": "Something something 2",
    "SomeOtherImportantMetadata3": "Something something 3"
  },
  { 
    ...
  }
]

Upvotes: 1

Views: 3262

Answers (1)

Daniel T.
Daniel T.

Reputation: 33967

The solution here is a two phase approach. First you should define two different structs for the two network calls and a third struct for the combined result. Let's say you go with:

struct TrackBasic {
    let id: Int 
    let title: String 
}

struct TrackMetadata {
    let id: Int // or whatever it's called.
    let playbackURL: String
}

struct Track {
    let id: Int 
    let title: String 
    let playbackURL: String
}

And define your functions like so:

func searchTracks(query: String) -> Observable<[TrackBasic]>
func getTracksMetadata(tracks: [Int]) -> Observable<[TrackMetadata]>

Now you can make the two calls and wrap the data from the two separate endpoints into the combined struct:

searchText
    .flatMapLatest { searchTracks(query: $0) }
    .flatMapLatest { basicTracks in
        Observable.combineLatest(Observable.just(basicTracks), getTracksMetadata(tracks: basicTracks.map { $0.id }))
    }
    .map { zip($0.0, $0.1) }
    .map { $0.map { Track(id: $0.0.id, title: $0.0.title, playbackURL: $0.1.playbackURL) } }

The above assumes that the track metadata comes in the same order that it was requested in. If that is not the case then the last map will have to be more complex.

Upvotes: 2

Related Questions