Объедините результаты двух вызовов API, выбирающих разные свойства для одних и тех же объектов, с помощью RxSwift - PullRequest
0 голосов
/ 16 ноября 2018

У меня есть модель под названием Track . Он имеет набор базовых и набор расширенных свойств. Список треков и их основные свойства выбираются с помощью API-вызова search , затем мне нужно выполнить еще один API-вызов с этими идентификаторами треков , чтобы получить их расширенные свойства.

Вопрос в том, как наилучшим образом объединить результаты обоих вызовов API и заполнить расширенные свойства в уже созданных объектах Track и, конечно, сопоставить их по идентификатору (который, к сожалению, другое имя свойства в результатах обоих вызовов). Обратите внимание, что в реальных наборах результатов возвращается гораздо больше ключей - около 20-30 свойств для каждого из двух вызовов.

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: [])
}

Результат JSON для вызова API .search структурирован следующим образом:

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

Результат JSON для вызова API .getTracksMetadata имеет следующую структуру:

[
  {
    "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"
  },
  { 
    ...
  }
]

1 Ответ

0 голосов
/ 16 ноября 2018

Решением здесь является двухфазный подход.Сначала вы должны определить две разные структуры для двух сетевых вызовов и третью структуру для комбинированного результата.Допустим, вы идете с:

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
}

И определяете свои функции следующим образом:

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

Теперь вы можете сделать два вызова и обернуть данные из двух отдельных конечных точек в объединенную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) } }

Вышеприведенное предполагает, что метаданные трека располагаются в том же порядке, в котором они были запрошены. Если это не так, то последняя карта должна быть более сложной.

...