Search code examples
swiftreactive-programmingrx-swiftchaining

Chain requests in RxSwift using first request to setup further requests then return all when complete


I have a requirement to request an Article.

Each Article contains an array of ArticleAsset which has various props on I need when rendering an entire article.

I do not know ahead of time how many assets exist on an article, so I must request the article and then using the assets prop dispatch X amount of request to return the value of each ArticleAsset.

At that point I should then return both the article and the array of results for my asset fetches.

For simplicity imagine in this case each asset returns an Int. So I start with this -

Article > [Article]

I would expect to end up with an tuple of the following shape (article: Article, assets: [Int])

I have attempted to recreate this as the below playground, but have been completely unsuccessful and am a little stuck.

I understand how to chain a fixed number of requests, using flatMapLatest etc but in this case I do not know the number of requests. I'm thinking I should map each ArticleAsset and return an array of Observables however I start to get very fuzzy on where to go next.

Any help would be much appreciated please and thank you.

import UIKit
import RxSwift

private let disposeBag = DisposeBag()

struct Article {
       let id: UUID = UUID()
       var assets: [ArticleAsset]
   }

   struct ArticleAsset {
       let number: Int
   }

   let assets: [ArticleAsset] = Array(0...4).map { ArticleAsset(number: $0) }
   let article = Article(assets: assets)

   func fetchArticle() -> Observable<Article> {
       return Observable.of(article)
   }

   func getArticleAsset(asset: ArticleAsset) -> Observable<Int> {
       return .of(asset.number)
   }

   fetchArticle()
       .map { art in
           let assets = art.assets.map { getArticleAsset(asset: $0) }
           let resp = (article: art, assets: Observable.of(assets))
           return resp
   }.subscribe(onNext: { resp in

     // I would like my subscriber to receive (article: Article, assets: [Int])

   }).disposed(by: disposeBag)

Solution

  • Kudos in making a compilable playground! That makes things a lot easier. What you want to do here is combine observables. In my article you will see that there are a lot of ways to do that. I think for this use-case the zip operator is best.

    let articleWithAssets = fetchArticle()
        .flatMap { (article) -> Observable<(article: Article, assets: [Int])> in
            let articles = Observable.zip(article.assets.map { getArticleAsset(asset: $0) })
            return Observable.zip(Observable.just(article), articles) { (article: $0, assets: $1) }
        }
    
    articleWithAssets
        .subscribe(onNext: { resp in
            // here `resp` is of type `(article: Article, assets: [Int])` as requested.
        })
    

    When fetchArticle() emits a value, the flatMaps closure will be called and it will call getArticleAsset(asset:) for each asset, wait until they are all done, combine them into a single Observable array (the articles object) and then combine that with the .just(article) observable.

    Warning though, if any one asset request fails, the entire chain fails. If you don't want that, you will have to take care of that inside the { getArticleAsset(asset: $0) } block. (Maybe emit a nil or missingAsset asset instead.)