Chained Network Request RXSwift

Viewed 503

Hey I learned on an HackingWithSwift Tutorial How to make a chained network request with Combine (see code below). Now I will build the same logic with RXSwift but I don't know how can I get/subscribe like in Combine to get the end result.

Combine:

//Combine code
func fetch<T: Decodable>(_ url: URL, defaultValue: T) -> AnyPublisher<T, Never> {
       let decoder = JSONDecoder()
       decoder.dateDecodingStrategy = .iso8601

       return URLSession.shared.dataTaskPublisher(for: url)
           .retry(1)
           .map(\.data)
           .decode(type: T.self, decoder: decoder)
           .replaceError(with: defaultValue)
           .receive(on: DispatchQueue.main)
           .eraseToAnyPublisher()
   }

//call fetch method and get the end result 
fetch(url, defaultValue: [URL]())
        .flatMap { urls in
           urls.publisher.flatMap { url in
              fetch(url, defaultValue: [NewsItem]())
           }
        }
        .collect()
        .sink { values in
            let allItems = values.joined()
            items = allItems.sorted { $0.id > $1.id }
        }
        .store(in: &requests)



//RXSwift code
func fetchWithRX<T: Decodable>(_ url: URL, defaultValue: T) -> Observable<T> {
       let decoder        = JSONDecoder()
       decoder.dateDecodingStrategy = .iso8601
       let request        = URLRequest(url: url)
       
       return URLSession.shared.rx.response(request: request)
           .retry(1)
           .map(\.data)
           .decode(type: T.self, decoder: decoder)
           .debug()
           .catchAndReturn(defaultValue)
           .observe(on: MainScheduler.instance)
   }


//call fetch2 method 
Now I want to subscribe to the values like in the first fetch method with flatMap..collect..sink etc.

fetchWithRX(url, defaultValue:  [URL]()) 

1 Answers

I would write the analog like this:

fetchWithRX(url, defaultValue: [URL]())
    .flatMap { urls in
        Observable.zip(urls.map { fetchWithRX($0, defaultValue: [NewsItem]()) })
    }
    .map { $0.flatMap { $0 }.sorted { $0.id > $1.id } }
    .subscribe(onNext: { values in
        items = values
    })
    .disposed(by: requests)

This way, I'm moving all the logic into a map closure which could be moved into a function for testability. Minimize the amount of code in a flatMap or subscribe in order to increase testability of your code.

Or you could write it like this:

fetchWithRX(url, defaultValue: [URL]())
    .flatMap { urls in
        Observable.zip(urls.map { fetchWithRX($0, defaultValue: [NewsItem]()) })
    }
    .subscribe(onNext: { values in
        let allItems = values.joined()
        items = allItems.sorted { $0.id > $1.id }
    })
    .disposed(by: requests)

You can learn more about combining observables in this article: Recipes for Combining Observables in RxSwift

URLSession also has an operator data(request:) which will just emit the data so you don't have to map to dump the result object. Like this:

func fetchWithRX<T: Decodable>(_ url: URL, defaultValue: T) -> Observable<T> {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    return URLSession.shared.rx.data(request: URLRequest(url: url))
        .retry(1)
        .decode(type: T.self, decoder: decoder)
        .catchAndReturn(defaultValue)
        .observe(on: MainScheduler.instance)
}

It occurs to me that you might be looking for a direct corollary to the original Combine code... The above samples will have the same eventual output but are subtly different in how they work...

Here is a direct translation:

fetchWithRX(url, defaultValue: [URL]())
    .flatMap { urls in
        // Observable.from(urls) works like urls.publisher 
        Observable.from(urls).flatMap { url in
            fetchWithRX(url, defaultValue: [NewsItem]())
        }
    }
    .toArray() // works like collect(). However, toArray() returns a Single rather than a generic Observable.
    .subscribe(onSuccess: { values in
        let allItems = values.joined()
        items = allItems.sorted { $0.id > $1.id }
    })
    .disposed(by: requests)

The difference is that the other examples preserve the order of the news items while this doesn't. Since you are collecting and sorting anyway, the eventual output is the same. You would only see a difference if you weren't using collect()/toArray() before observing the output.

Related