Search code examples
swiftreactive-programmingrx-swift

RxSwift: delay observable until another observable is finished?


I load posts from the server and want to show them only when their images are all loaded. Here's what I was trying to do:

postsRepository
    .fetchPosts()
    .flatMap { posts -> Observable<[Post]> in
        return Observable.merge(posts.map({ /* Code that returns Observable<UIImage> */ }))
            .takeLast(1)
            .map { _ in posts }
    }

However, this code above loads images twice: once all the posts are fetched and once I bind posts to the table view. How to deal with that?


Solution

  • This was a fun one...

    let postsWithImages = postsRepository
        .fetchPosts()
        .flatMap { posts in
            Observable.combineLatest(
                posts
                    .map { $0.imageURL }
                    .map { URLSession.shared.rx.data(request: URLRequest(url: $0))
                        .map { UIImage(data: $0) }
                        .catchErrorJustReturn(nil)
                    }
                )
                .map { zip(posts, $0) }
        }
        .map { $0.map { (post: $0.0, image: $0.1) } }
    

    That last map might not be strictly necessary if you are comfortable with working with a Zip2Sequence.

    The above assumes that Post has an imageURL: URL property.


    Let's clean that up a bit:

    let postsWithImages = postsRepository
        .fetchPosts()
        .flatMap { posts in
            Observable.combineLatest(
                posts
                    .map { $0.imageURL }
                    .map { URLSession.shared.rx.image(for: $0) }
                )
                .map { zip(posts, $0) }
        }
        .map { $0.map { (post: $0.0, image: $0.1) } }
    

    The above requires a new function:

    extension Reactive where Base == URLSession {
        func image(for url: URL) -> Observable<UIImage?> {
            return data(request: URLRequest(url: url))
                .map { UIImage(data: $0) }
                .catchErrorJustReturn(nil)
        }
    }
    

    I figured I would add some explanation. As I mentioned in my article, Recipes for Combining Observables in RxSwift, the magic here is the use of combineLatest to convert an [Observable<T>] into an Observable<[T]>.

    The operator takes the array of Observables, subscribes to all of them, waits for them all to complete, then emits an event with an array of all the values gathered. (After that it would emit a new array each time one of the items updated, but that isn't relevant here.)