Home · iOS & Swift Tutorials

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.

Version

  • Swift 5, iOS 13, Xcode 11
Update note: Ehab Yosry Amer updated this tutorial for iOS 13, Xcode 11 and, Swift 5. Yono Mittlefehldt wrote the original.

Each year, the newest iPhone camera gets better and better. But do you know who has the best cameras? No, not the modeling agency that does your headshots. It’s NASA!

And what’s the best subject matter for them to photograph? Again, unless you work for NASA, the answer is, unfortunately, not you. It’s SPACE! But you’re a close second. :]

Wouldn’t it be nice to have an app that displays NASA’s beautiful photos so you could look at them on your phone whenever you want? The only problem is that some of these photos of space are enormous — like 60MB enormous. And to top it off, they don’t have thumbnails available.

You already dread having to write the tons of boilerplate code associated with OperationQueue. If only there were a better way.

Good news! There is a better way in the form of a third-party library called Nuke.

In this Nuke tutorial, you’ll learn how to:

  • Integrate Nuke into your project using the Swift Package Manager.
  • Load remote images while maintaining the app’s responsiveness.
  • Resize photos on the fly to keep memory usage under control.
  • Integrate Nuke’s Combine extension called ImagePublisher.
  • Use Combine in your project to load different image sizes consecutively.

Ready to explore space from the comfort of your iPhone? Time to get started!

Getting Started

Click the Download Materials button at the top or bottom of this tutorial to download the resources you’ll need. Open the starter project, then build and run. Take a look around the app at your leisure.

The Far Out Photos app grabs photos from NASA’s website and displays them in a UICollectionView-based gallery. Tapping one of the beautiful photos brings it up in full screen.

Unfortunately, there’s a major problem with this app. It’s slow and unresponsive. This is because all network requests for images are done on the main thread, so they interfere with your ability to scroll. Additionally, there’s no caching so images are re-fetched each time they’re loaded.

Annoyed yellow monster with closed eyes

You’re going to use Nuke to fix this app and get it running smoothly.

The former planet Pluto

Setting up Nuke

You have a few options for integrating Nuke into your project, but for this tutorial, you’ll use Swift Package Manager, or SwiftPM. It’s an easy way to install packages in your project that Apple introduced in Xcode 11.

To learn more about SwiftPM, check out the Swift Package Manager for iOS tutorial.

Installing with Swift Package Manager

To add a package, go to File ▸ Swift Packages ▸ Add Package Dependency.

Adding a package with SwiftPM

This opens a dialog box to choose the package you want to install. Enter the URL https://github.com/kean/Nuke.git in the text field and click Next.

Nuke URL in SwiftPM Dialogue

Select Version for the rule, and with Up to Next Major from the drop-down menu, and enter 9.1.0 in the box. This specifies that the version will be a minimum of 9.1.0 and up to a maximum of, but not including, 10.0.0. Then select Next and wait for Xcode to verify the package.

Nuke version settings in SwiftPM

Make sure the target Far Out Photos is selected and click Finish.

Make sure the target is selected

Your Project navigator should look like this when the package is installed.

Project Navigation after package is installed

With that done, it’s time to dive into Nuke.

Note: You may see a different patch number — the third number in the version — since Nuke is still under active development.

Using Nuke: Basic Mode

The first thing you’re going to fix is the terrible responsiveness of the app when scrolling through the gallery.

To get started, open PhotoGalleryViewController.swift. Add the following to the top of the file:

import Nuke

Next, find collectionView(_:cellForItemAt:). In this method, you should see the following code:

if let imageData = try? Data(contentsOf: photoURLs[indexPath.row]),
  let image = UIImage(data: imageData) {
    cell.imageView.image = image
} else {
  cell.imageView.image = nil
}

This code inefficiently fetches the photo from the provided URL and assigns it to the image view within the collection view cell, blocking the main thread in the whole process.

Delete those seven lines and replace them with the following:

// 1
let url = photoURLs[indexPath.row]

// 2
Nuke.loadImage(with: url, into: cell.imageView)

In this code, you:

  1. Grab the URL for the correct photo based on the cell’s index path.
  2. Use Nuke to load the image from the URL directly into the cell’s image view. Nuke is doing all of the heavy lifting under-the-hood. It loads the image on a background thread and assigns it to the image view.

That’s it! Much easier than expected right? :]

Build and run. You should notice one major improvement. You can now smoothly scroll the gallery view!

Smooth scrolling in gallery after using Nuke

Setting Loading Options

That’s a great start, but there’s something very important still missing. The app doesn’t give the user visual feedback that images are coming. Instead, if users scroll fast enough, they’ll only be greeted with an almost solid black screen.

If only Nuke had some way to show an image in place of the loading image, a placeholder if you will.

Today is your lucky day. It does!

Nuke has a struct called ImageLoadingOptions, which allows you to change how Nuke presents images, including setting a placeholder image.

Back in collectionView(_:cellForItemAt:), replace the code you just wrote with the following:

let url = photoURLs[indexPath.row]

// 1
let options = ImageLoadingOptions(
  placeholder: UIImage(named: "dark-moon"),
  transition: .fadeIn(duration: 0.5)
)
   
// 2 
Nuke.loadImage(with: url, options: options, into: cell.imageView)

Here you:

  1. Create an ImageLoadingOptions struct. You set the placeholder image to the image named dark-moon and configure the transition from the placeholder image to the image fetched from the network. In this case, you set the transition as a fade-in over 0.5 seconds.
  2. Use Nuke to load the image from the URL directly into the cell’s image view, just like before, but this time using the options you just configured.

Now, build and run the app. You should see the placeholder images fade to reveal the real images as they are loaded.

A placeholder image is shown until the original is loaded and the cross fade

Fantastic job! You’ve already greatly improved the app with just a few lines of code.

Monitoring Memory Usage

How’s the app working for you, so far? Have you experienced any crashes?

If you’ve been running this project on a device, there’s a fairly good chance you’ve experienced a crash or two. Why? Because this app is a memory hog.

memory hog

Using Instruments

To see how bad it is, run the app again, and then do the following:

  1. Select the Debug navigator in the Navigator panel.
  2. Then select Memory under the list of debug gauges.
  3. Click Profile in Instruments.
  4. Finally, click Restart in the dialog that appears.

Nuke tutorial - Memory 1-3

Nuke tutorial - Memory 4

With the profiler running, scroll the gallery to the bottom, paying particular attention to the Persistent Bytes column of the row labeled VM: CG raster data. Over a gigabyte of data is being kept around!

Nuke tutorial - Memory Usage

The source of the problem is that, even though the downloaded images look small on the screen, they’re still full-sized images and stored completely in memory. That’s not good.

Unfortunately, NASA doesn’t provide a thumbnail size for its images.

What to do? Maybe Nuke can help?

Indeed it can!

Advanced Nuking

Nuke has many capabilities you can use to optimize your memory and improve your app’s user experience and loading times.

Loading with Requests

So far, you’ve been passing loadImage a URL. But Nuke also has a variation of that method that accepts an ImageRequest.

ImageRequest can define a set of image processors to be applied after the image is downloaded. Here you’ll create a resizing processor and attach it to the request.

In PhotoGalleryViewController.swift right after the definition of the photoURLs instance variable, add these two calculated properties:

// 1
var pixelSize: CGFloat {
  return cellSize * UIScreen.main.scale
}

// 2
var resizedImageProcessors: [ImageProcessing] {
  let imageSize = CGSize(width: pixelSize, height: pixelSize)
  return [ImageProcessors.Resize(size: imageSize, contentMode: .aspectFill)]
}

This is what your new code does:

  1. pixelSize is the size in pixels of the cell. Some iPhones have a 2x resolution (2 pixels per point) and others have 3x resolutions (3 pixels per point). You want to have your images looking sharp and not pixelated on your high-resolution screens. This multiplier is also known as the device’s scale.
  2. resizedImageProcessors is a Nuke configuration that defines what operations you want to do on images. For now, you only want to resize the images to fit your cells and use an aspect fill as a content-mode.

Returning to collectionView(_:cellForItemAt:), replace the call to Nuke.loadImage(with:options:into:) with the following:

// 1
let request = ImageRequest(
  url: url,
  processors: resizedImageProcessors)

// 2
Nuke.loadImage(with: request, options: options, into: cell.imageView)

With this code, you:

  1. Create an ImageRequest for the desired image URL, and use the image processor you defined earlier to apply a resize on the image after it is downloaded.
  2. Have Nuke load the image based on this request, using the options you previously set, and show it in the cell’s image view.

Now, build and run again, and open the memory profiler the same way you did before.

A graph and table showing reduced memory usage

Wow! The VM: CG raster data is now under 300MB! That’s a much more reasonable number! :]

Optimizing Code

Currently, for every collection view cell, you’re re-creating the same ImageLoadingOptions. That’s not super efficient.

One way to fix this would be to create a constant class property for the options you’re using and pass that to Nuke’s loadImage(with:options:into:) each time.

Nuke has a better way to do this. In Nuke, you can define ImageLoadingOptions as the default value when no other options are provided.

In PhotoGalleryViewController.swift, add the following code to the bottom of viewDidLoad()

// 1
let contentModes = ImageLoadingOptions.ContentModes(
  success: .scaleAspectFill, 
  failure: .scaleAspectFit, 
  placeholder: .scaleAspectFit)

ImageLoadingOptions.shared.contentModes = contentModes

// 2
ImageLoadingOptions.shared.placeholder = UIImage(named: "dark-moon")

// 3
ImageLoadingOptions.shared.failureImage = UIImage(named: "annoyed")

// 4
ImageLoadingOptions.shared.transition = .fadeIn(duration: 0.5)

In this code, you:

  1. Define the default contentMode for each type of image loading result: success, failure and the placeholder.
  2. Set the default placeholder image.
  3. Set the default image to display when there’s an error.
  4. Define the default transition from placeholder to another image.

With that done, go back to collectionView(_:cellForItemAt:). Here, you need to do two things.

First, remove the following lines of code:

let options = ImageLoadingOptions(
  placeholder: UIImage(named: "dark-moon"),
  transition: .fadeIn(duration: 0.5)
)

You won’t need these anymore, because you defined default options. Then you need to change the call to loadImage(with:options:into:) to look like this:

Nuke.loadImage(with: request, into: cell.imageView)

If you build and run the code now, you probably won’t see much of a difference, but you did sneak in a new feature while improving your code.

Turn off your Wi-Fi and run the app once more. You should start to see an angry and frustrated little alien appear for each image that fails to load.

An angry and frustrated little alien appear for each image that fails to load

Besides adding a failure image, you should feel content knowing that your code is smaller and cleaner!

Using ImagePipeline to Load Images

OK, you need to solve another problem.

Currently, when you tap an image in the gallery, the image to display in the detail view is fetched on the main thread. This, as you already know, blocks the UI from responding to input.

If your internet is fast enough, you may not notice any issue for a few images. However, scroll to the very bottom of the gallery. Check out the image of Eagle Nebula, which is the middle image, third row from the bottom:

Image of Eagle Nebula in the gallery

The full size image is about 60 MB! If you tap it, you will notice your UI freeze.

To fix this problem, you’re going to use — wait for it — Nuke. However, you won’t be using loadImage(with:into:). Instead, you’ll use something different to understand the different ways you can utilize Nuke.

Open to PhotoViewController.swift. Import Nuke at the top of the file.

import Nuke

Find the following code in viewDidLoad()

if let imageData = try? Data(contentsOf: imageURL),
  let image = UIImage(data: imageData) {
  imageView.image = image
}

This is the same naive image loading you saw before. Replace it with the following code:

// 1
imageView.image = ImageLoadingOptions.shared.placeholder
imageView.contentMode = .scaleAspectFit

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

In this code, you:

  1. Set the placeholder image and content mode.
  2. Call loadImage(with:) on the ImagePipeline singleton.
  3. Pass in the appropriate photo URL.
  4. Provide a completion handler. The handler has a parameter of an enum type Result<ImageResponse, Error>.
  5. The response can have only two values: .success with an associated value of type ImageResponse, or .failure with an associated value of type Error. So a switch statement will work best to check for both possible values.
  6. In the failure case, set the image to the appropriate failure image.
  7. For success, set the image to the downloaded photo.

There! It’s time. Build and run and tap the Eagle Nebula photo once again.

Eagle Nebula full image is loaded smoothly

No more UI freezing! Great work.

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()
}

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.

Chaining Requests

So far, you have one publisher defined, but you want to use two publishers — one for each image request — and chain the two to execute them sequentially.

In PhotoViewController.swift within loadImage(url:), right after resizedImagePublisher is defined, add this line.

let originalImagePublisher = ImagePipeline.shared.imagePublisher(with: url)

This creates a publisher directly using the image URL without any image processors.

Next, replace:

cancellable = resizedImagePublisher
  .sink(

with:

cancellable = resizedImagePublisher.append(originalImagePublisher)
  .sink(

The addition of .append(:) on a publisher creates a new publisher that combines resizedImagePublisher and originalImagePublisher. Thus, you can treat it the same way, and internally it will make each publisher work and finish before going to the next one.

Build and run. Tap any image from the gallery and see it opening with an image, then in front of your eyes showing a sharper image.

Sharper image replacing the existing one.

Note: If you don’t see the transition clearly, reduce the image size you are requesting in the gallery in PhotoGalleryViewController.resizedImageProcessors to reduce the quality of the first image.

Cleaning the Subscriber

Earlier you learned that in the subscriber, you get notified once with the result of the publisher if it finished or failed, and only if it finished do you get notified with a value.

This means that you are responding in two different places, although all you are doing is just showing a different image and a different content mode.

Why not make the publisher just provide the image and the content mode to use, so all you have to do is just display those values. More accurately: Make the publisher provide a value even if it failed.

The publisher you are using returns an object that has the image among other things but doesn’t have anything about the content mode. That is something unique to this app.

So the first thing you want to do is tell the publisher to provide a different value type, instead of ImageResponse, which is a type defined in ImagePublisher. You want to use a tuple (Image, UIView.ContentMode)

Back to PhotoViewController. Right before the call to .sink(receiveCompletion:receiveValue:) add the following:

.map {
  ($0.image, UIView.ContentMode.scaleAspectFill)
}

map(_:) does a transformation for you. You are using the original value received from the publisher and converting this value to a tuple. And since there is a value, this means the publisher succeeded, and all the NASA images should have scaleAspectFill as their content mode.

Second, you want to intercept the publisher if an error occurred that would cause it to provide a failure. Instead, make the publisher give you the failure image and aspect fit in a tuple.

After the definition of originalImagePublisher, add:

guard let failedImage = ImageLoadingOptions.shared.failureImage else {
  return
}

Then, right after the closing braces for map(_:) and before the call to .sink(receiveCompletion:receiveValue:), add:

.catch { _ in
  Just((failedImage, .scaleAspectFit))
}

The publisher calls .catch(_:) whenever it fails. The call to Just(_:) creates a publisher that always sends the value given and then completes. In this case, it’s the default failure image you specified in the global ImageLoadingOptions. The catch(_:) means that any error from the image publishers will be “caught” and then the failure replaced with the failure image.

Finally, replace all the code you have for .sink(receiveCompletion:receiveValue:) with:

.sink {
  self.imageView.image = $0
  self.imageView.contentMode = $1
}

Your subscriber will always receive a value because you managed to transform the publisher to one that never fails. And the result type it now provides is much more convenient for your app. So naturally, the code that is checking if it succeeded or not is not needed, and all you need is “Just” receive the value. :]

Build and run the app. It will work just as before, but now you have some simpler Combine code!

Where to Go From Here?

Congratulations on making it this far! You’ve learned quite a bit about how to use Nuke to improve your app’s performance!

Download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

There are still more features in Nuke you can explore. For instance, Nuke has support for animated GIFs. You can also go deeper into the cache management topic, if your app needs that level of control.

Additionally, several plug-ins can extend the functionality of Nuke, such as support for SwiftUI , RxSwift, WebP, and Alamofire.

And you can always learn more about Combine from the tutorial Combine: Getting Started.

If you enjoyed learning about Combine, then you might be interested in our Combine: Asynchronous Programming with Swift book.

I hope you enjoyed this Nuke tutorial and, if you have any questions or comments, please join the forum discussion below!

Add a rating for this content

More like this

Contributors

Comments