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 2 of 4 of this article. Click here to view the first page.

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.