I’m excited to introduce a new addition to Nuke - RxNuke which brings the power of RxSwift to your image loading pipelines.
Nuke’s design has always prioritized simplicity, reliability, and performance. The core framework has a small API surface and only contains a minimum number of features to built upon. This is great for many reasons, but unfortunately, that left a gap between what Nuke supports out of the box and what users need. I received many requests about particular use cases like these:
- “show stale response while validating the image”
- “wait until N images are loaded and only then proceed”
- “show a low-res image while loading a high-res one”.
Each app has a different idea about how to configure their image loading pipeline. It’s not feasible to support all of them in a single framework. RxNuke aims at bridging this gap by leveraging the power of reactive programming to serve all of the aforementioned use cases, as well as many others.
Check out API Client in Swift for more awesome use-cases of RxSwift. You could also find Smart Rerty useful. It can automatically retry image requests for you.
Introduction #
In order to get started with RxNuke you should be familiar with the basics of RxSwift. Even if you don’t you can already start taking advantage of RxNuke powerful features thanks to a number of examples of common use cases available in a RxNuke documentation.
Let’s starts with the basics. The initial version of RxNuke adds a single new Loading
protocol with a set of methods which returns Singles
.
public protocol Loading {
func loadImage(with url: URL) -> Single<Image>
func loadImage(with urlRequest: URLRequest) -> Single<Image>
func loadImage(with request: Nuke.Request) -> Single<Image>
}
A
Single
is a variation ofObservable
that, instead of emitting a series of elements, is always guaranteed to emit either a single element or an error. The common use case ofSingle
is to wrap HTTP requests. See Traits for more info.
One of the Nuke’s types that implement new Loading
protocol is Nuke.Manager
. Here’s an example of how to start a request using one of the new APIs and then display an image if the request is finished successfully:
Nuke.Manager.shared.loadImage(with: url)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { imageView.image = $0 })
.disposed(by: disposeBag)
The first thing that Nuke.Manager
does when you subscribe to an observable is check if the image is stored in its memory cache. If it is the manager synchronously successfully finishes the request. If the image is not cached, the manager asynchronously loads an image using an underlying image loader (see Nuke.Loading
protocol).
This looks simple enough. Now let’s see what makes this new addition to Nuke so powerful.
Use Cases #
I’m going to go through a number of real world uses case and see how they can be implemented using RxNuke
:
- Going From Low to High Resolution
- Loading the First Available Image
- Load Multiple Images, Display All at Once
- Showing Stale Image While Validating It
- Auto Retry
- Tracking Activities
- Table or Collection View
Going From Low to High Resolution #
Suppose you want to show users a high-resolution, slow-to-download image. Rather than let them stare a placeholder for a while, you might want to quickly download a smaller thumbnail first.
You can implement this using concat
operator which results in a serial execution. It would first start a thumbnail request, wait until it finishes, and only then start a request for a high-resolution image.
Observable.concat(loader.loadImage(with: lowResUrl).orEmpty,
loader.loadImage(with: highResUtl).orEmpty)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { imageView.image = $0 })
.disposed(by: disposeBag)
orEmpty
is a custom operator which dismisses errors and completes the sequence instead (equivalent tofunc catchErrorJustComplete()
from RxSwiftExt
Loading the First Available Image #
Suppose you have multiple URLs for the same image. For instance, you might have uploaded an image taken from the camera. In such case, it would be beneficial to first try to get the local URL, and if that fails, try to get the network URL. It would be a shame to download the image that we may have already locally.
This use case is very similar Going From Low to High Resolution, but an addition of .take(1)
guarantees that we stop execution as soon as we receive the first result.
Observable.concat(loader.loadImage(with: localUrl).orEmpty,
loader.loadImage(with: networkUrl).orEmpty)
.take(1)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { imageView.image = $0 })
.disposed(by: disposeBag)
Load Multiple Images, Display All at Once #
Suppose you want to load two icons for a button, one icon for .normal
state and one for .selected
state. Only when both icons are loaded you can show the button to the user. This can be done using a combineLatest
operator:
Observable.combineLatest(loader.loadImage(with: iconUrl).asObservable(),
loader.loadImage(with: iconSelectedUrl).asObservable())
.observeOn(MainScheduler.instance)
.subscribe(onNext: { icon, iconSelected in
button.isHidden = false
button.setImage(icon, for: .normal)
button.setImage(iconSelected, for: .selected)
}).disposed(by: disposeBag)
Showing Stale Image While Validating It #
Suppose you want to show users a stale image stored in a disk cache (Foundation.URLCache
) while you go to the server to validate it. This use case is actually the same as Going From Low to High Resolution.
let cacheRequest = URLRequest(url: imageUrl, cachePolicy: .returnCacheDataDontLoad)
let networkRequest = URLRequest(url: imageUrl, cachePolicy: .useProtocolCachePolicy)
Observable.concat(loader.loadImage(with: cacheRequest).orEmpty,
loader.loadImage(with: networkRequest).orEmpty)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { imageView.image = $0 })
.disposed(by: disposeBag)
See Image Caching to learn more about HTTP cache
Auto Retry #
Auto-retry up to 3 times with an exponentially increasing delay using a retry operator provided by RxSwiftExt.
loader.loadImage(with: request).asObservable()
.retry(.exponentialDelayed(maxCount: 3, initial: 3.0, multiplier: 1.0))
.observeOn(MainScheduler.instance)
.subscribe(onNext: { imageView.image = $0 })
.disposed(by: disposeBag)
See A Smarter Retry with RxSwiftExt for more info about auto retries
Tracking Activities #
Suppose you want to show an activity indicator while waiting for an image to load. Here’s how you can do it using ActivityIndicator
class provided by RxSwiftUtilities
:
let isBusy = ActivityIndicator()
loader.loadImage(with: imageUrl)
.observeOn(MainScheduler.instance)
.trackActivity(isBusy)
.subscribe(onNext: { imageView.image = $0 })
.disposed(by: disposeBag)
isBusy.asDriver()
.drive(activityIndicator.rx.isAnimating)
.disposed(by: disposeBag)
Table or Collection View #
Here’s how you can integrate the code provided in the previous examples into your table or collection view cells:
final class ImageCell: UICollectionViewCell {
private var imageView: UIImageView!
private var disposeBag = DisposeBag()
// <.. create an image view using your preferred way ..>
func display(_ image: Single<Image>) {
// Create a new dispose bag, previous dispose bag gets deallocated
// and cancels all previous subscriptions.
disposeBag = DisposeBag()
imageView.image = nil
// Load an image and display the result on success.
image.subscribeOn(MainScheduler.instance)
.subscribe(onSuccess: { [weak self] image in
self?.imageView.image = image
}).disposed(by: disposeBag)
}
}
Conclusion #
I hope that RxNuke
becomes a valuable addition to Nuke. It brings power to solve many common use cases which are hard to implement without Rx. RxNuke
is still very early stage. As it evolves it’s going to bring some new powerful features made possible by Rx, more examples of common use cases, and more Nuke
extensions to give you the power to build the exact image loading pipelines that you want.
If you have any questions, additions or corrections to the examples from the article please feel free to leave a comment below, or hit me up on Twitter.