Nuke Tutorial for iOS: Getting Started

In this Nuke tutorial, you’ll learn how to integrate Nuke using Swift Package Manager and use it to load remote images, both with and without Combine. By Ehab Amer.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Caching Images

Nuke has a cool feature that allows you to aggressively cache images. What does this mean? Well, it means it will ignore the Cache-Control directives that may be found in HTTP headers.

But why would you want to do this? Sometimes, you know that the data is not going to change. This is often the case for images found on the web. Not always, but often.

If your app is working with images that you know shouldn’t change, it’s a good idea to set up Nuke to use an aggressive image cache and not risk reloading the same images.

In PhotoGalleryViewController.swift, go back to viewDidLoad() and add this code to the end:

// 1
DataLoader.sharedUrlCache.diskCapacity = 0
    
let pipeline = ImagePipeline {
  // 2
  let dataCache = try? DataCache(name: "com.raywenderlich.Far-Out-Photos.datacache")
      
  // 3
  dataCache?.sizeLimit = 200 * 1024 * 1024
      
  // 4
  $0.dataCache = dataCache
}

// 5
ImagePipeline.shared = pipeline

Here, you:

  1. Disable the default disk cache by setting its capacity to zero. You don’t want to accidentally cache the image twice. You’ll create your own cache.
  2. Create a new data cache.
  3. Set the cache size limit to 200 MB (since there are 1,024 bytes per kilobyte and 1,024 kilobytes per megabyte).
  4. Configure the ImagePipeline to use the newly created DataCache.
  5. Set this image pipeline to be the default one used when none is specified.

That’s it!

If you build and run your code now, you won’t see a difference. However, behind the scenes your app is caching all images to disk and using the cache to load anything it can.

Note: If you want to clear the cache manually, it’s simple, but not straightforward. You need to do some typecasting due to how things are internally stored in an ImagePipeline:
if let dataCache = ImagePipeline.shared
  .configuration.dataCache as? DataCache {
    dataCache.removeAll()
}
if let dataCache = ImagePipeline.shared
  .configuration.dataCache as? DataCache {
    dataCache.removeAll()
}

Combining with Combine

Nuke supports integration with other frameworks through its extensions. In the next part, you’ll learn how to combine using Nuke with the Combine framework. :]

Combine is a framework Apple released with iOS 13 to provide declarative APIs through publisher-subscriber structure. It greatly facilitates the handling of asynchronous events and allows you to combine event-processing operations.

Setting up ImagePublisher

You need to install a Nuke extension named ImagePublisher. Install it the same way you did earlier for Nuke using the Swift Package Manager.

Add a new package from the URL: https://github.com/kean/ImagePublisher.git.

Note:At the time of writing this tutorial, the available version is 0.2.1.

Adding ImagePublisher extension for combine

Before you start using the new extension, note what’s currently happening in your app.

First, the gallery downloads all the images and stores a resized version of the images in its cache. Second, when you open any photo, the full image starts to download and, while that happens, you see a placeholder image.

So far that makes sense, but that is not the best experience you want to provide to your users. You are showing a generic placeholder image although you have a resized image already stored. Ideally, you want to show that photo instead until the full image is downloaded.

The best way to do this is to request the resized image, which most likely will be almost instantly available if it was already cached. Then, right after that request is finished, fetch the full-sized one. In other words: You want to chain the two requests.

But first, test your skills at basic requests with ImagePublisher.

Using ImagePublisher

In PhotoViewController.swift, add the following imports at the top of the file:

import Combine
import ImagePublisher

Then add the following properties at the top of the class.

//1
var cancellable: AnyCancellable?

//2
var resizedImageProcessors: [ImageProcessing] = []
  1. cancellable, in very simple terms, is a handle for an operation that — as its name says — is cancellable. You’ll use it to refer to the request operation you’ll create to download the images.
  2. resizedImageProcessors is like what you used in PhotoGalleryViewController when you built the request for the resized image. You want the PhotoGalleryViewController as it’s opening a new PhotoViewController to provide the same processors so you can make an identical request. If the request is different, then a new photo will download, but you want the same one showing in the gallery so you can fetch it from the cache whenever possible.

At the end of the class, add the new method

func loadImage(url: URL) {
  // 1
  let resizedImageRequest = ImageRequest(
    url: url,
    processors: resizedImageProcessors)

  // 2
  let resizedImagePublisher = ImagePipeline.shared
    .imagePublisher(with: resizedImageRequest)

  // 3
  cancellable = resizedImagePublisher
    .sink(
    // 4
      receiveCompletion: { [weak self] response in
        guard let self = self else { return }
        switch response {
        case .failure:
          self.imageView.image = ImageLoadingOptions.shared.failureImage
          self.imageView.contentMode = .scaleAspectFit
        case .finished:
          break
        }
      },
    // 5
      receiveValue: {
        self.imageView.image = $0.image
        self.imageView.contentMode = .scaleAspectFill
      }
  )
}

Here’s what this does:

  1. Create a new request with the provided image URL, using the processors you defined earlier.
  2. Create a new publisher for processing this request. A publisher is in effect a stream of values that either completes or fails. In the case of this publisher, it will process the request and then either return the image or fail.
  3. Execute this publisher, or in Combine lingo: Create a subscriber with sink(receiveCompletion:receiveValue:) on this publisher with two closures.
  4. The first closure provides the result from the publisher, whether it managed to finish its operation normally or it failed. In case of failure, you show the failure image with an aspect fit content mode. And if it finishes normally, then you do nothing here because you’ll receive the value in the second closure.
  5. Show the value you received normally with an aspect fill. Know that you don’t receive any values if the first closure had a failure response.

In viewDidLoad(), remove all of the following code that is responsible for loading the image:

ImagePipeline.shared.loadImage(
  with: imageURL) { [weak self] response in // 4
  guard let self = self else {
    return
  }
  switch response {
    case .failure:
      self.imageView.image = ImageLoadingOptions.shared.failureImage
      self.imageView.contentMode = .scaleAspectFit
    case let .success(imageResponse):
      self.imageView.image = imageResponse.image
      self.imageView.contentMode = .scaleAspectFill
    }
}

And replace it with the call to the newly added method:

loadImage(url: imageURL)

Now go to PhotoGalleryViewController.swift and find collectionView(_:didSelectItemAt:). Right before you push the view controller onto the navigation stack, add this line:

photoViewController.resizedImageProcessors = resizedImageProcessors

On your simulator, uninstall the app completely with a long press on the app icon, then choose Delete App. This completely deletes the cached images, among everything else related to the app, so you can start fresh.

Deleting an app from the simulator.

Build and run. Tap any image that has finished loading and see that it appears instantly without showing the loading placeholder. But the image will not be as sharp as it used to be. This is because you are loading the same resized image from the gallery and Nuke is providing it to you with the second request from the cache.

You’re not done yet. You want to load the full-sized image, too.