emenegro
emenegro

Reputation: 6971

RxSwift, chain dependent downloads returning same Observable type

I'm downloading a list of books from an API, and for each book I want to download its cover image.

I'm stuck with what I think it's a very simple and common task in Rx. I've done a research but I cannot find the solution because all the related questions are about how to return another Observable; for example get Github repositories and then get the issues for each one, but returning Observable<Issue> not a modified Observable<Repository>, that is my case. In my case I want to modify the previous observable result (Observable<[Book]>) with the result of another request returning different observable (Observable<Data>).

For now I have the first part, download books:

func search(query: String) -> Observable<[Book]> {
    return networkSession.rx
        .data(request: URLRequest(url: url))
        .map({ try JSONDecoder().decode([Book].self, from: $0) })
}

Book is a simple struct:

struct Book {
    let title: String
    let authors: [String]
    let coverImageURL: URL
    var coverImage: Data?
}

After that I'm able to download each image but I don't know how to assign it to each object in order to return the same Observable<[Book]> type without messing with nested observables and a zillion of compile time errors. I'm pretty sure this is a common scenario to use flatMap but it is still kind of obscure to me.

Thank you so much!

Upvotes: 2

Views: 897

Answers (1)

Stephan Schlecht
Stephan Schlecht

Reputation: 27106

Perhaps one should split the Book struct into two structs. The first one is called BookInfo here and an array of them is downloaded with calling the function search.

struct BookInfo {
    let title: String
    let authors: [String]
    let coverImageURL: URL
}

Composing a BookInfo instance together with the coverImage data could result in Book struct like this:

struct Book {
    let bookInfo: BookInfo
    let coverImage: Data
}

The RxSwift chain would look like this:

self.search(query: "<someQuery>") 
.flatMap { (bookInfos: [BookInfo]) -> Observable<BookInfo> in
    Observable.from(bookInfos)
}
.flatMap { (bookInfo: BookInfo)  -> Observable<Book> in
    //use the coverImageURL from the closure parameter and fetch the coverImage
    //return an Observable<Book>  
}
.observeOn(MainScheduler.instance)
.do(onNext: { (book: Book) in
    //e.g. add a book in main thread to the UI once the cover image is available
})
.subscribe()
.disposed(by: disposeBag)

Instead of using type inference I added explicit type specifications, so it is more obvious what kind of elements are passed through the chain.

UPDATE

If you want to use just one Book struct, you could use this:

self.search(query: "<someQuery>") 
.flatMap { (books: [Book]) -> Observable<Book> in
    Observable.from(books)
}
.flatMap { (book: Book)  -> Observable<Book> in
    //use the book.coverImageURL from the closure parameter and fetch the coverImage
    //since the closure parameter 'book' is a let constant you have to construct a new Book instance 
    //return an Observable<Book>     
}
...

Please note that the closure parameters are let constants. This means that you cannot change the coverImage property even though it is defined as var in the structure. Instead, you must create a new instance of the Book structure, fill it with the values from the Closure parameter, and add the coverImage data. Accordingly, if you want to go this route, you could also optionally change coverImage to be a let constant.

I guess I personally would a have a slight preference for the first approach with the two structs.

Upvotes: 1

Related Questions