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?
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.)