Nuke Tutorial for iOS: Getting Started

In this Nuke tutorial, you learn how to integrate Nuke into your project using CocoaPods and load remote images while maintaining the app’s responsiveness.

5/5 2 Ratings · Leave a Rating

Version

  • Swift 4.2, iOS 12, Xcode 10

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.

NASA!

And what’s the best subject matter for them to photograph? Again, unless you work for NASA, the answer is, unfortunately, not you.

SPACE!

But you are a close second. :]

It would 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 OperationQueues. 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 learn how to:

  • Integrate Nuke into your project using CocoaPods.
  • Load remote images while maintaining the app’s responsiveness.
  • Resize photos on the fly to keep memory usage under control.

Ready to explore space from the comfort of your iPhone?

Getting Started

Use the Download Materials button at the top or bottom of this tutorial to download the materials you’ll need. Open the starter project and explore a bit.

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

Unfortunately, there’s a major problem with the way this app is written. It’s slow and unresponsive. This is because all network requests for images are done on the main thread — you know, the one where the UI is supposed to refresh. Additionally, there’s no caching.

You’re going to use Nuke to fix this app, and get it running as smooth as the surface of Europa. Or maybe even as smooth as the heart on Pluto.

Setting Up Nuke

You have a few options for integrating Nuke into your project, but for this Nuke tutorial, you’ll use CocoaPods. A super easy way to use CocoaPods is to download the app.

After your download is complete, launch the CocoaPods app. You should see something like this:
Nuke tutorial - CocoaPods

Next, press Command-N. In the Open panel that pops up, navigate to the directory with the tutorial materials you downloaded, and choose Far Out Photos.xcodeproj from the starter directory.

The CocoaPods app will automatically create a Podfile in your project directory and then open it using a built-in editor.

Nuke tutorial - Podfile

Add the following line right before the end keyword:

pod 'Nuke', '~> 7.0'

Your Podfile should look something like this:

project 'Far Out Photos.xcodeproj'

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'Far Out Photos' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for Far Out Photos
  pod 'Nuke', '~> 7.0'
end

This code will instruct CocoaPods to use the latest version of Nuke, up to the next major version (8.0 in this case).

Now, press the Install button at the top-right corner, and wait for CocoaPods to do its thing.

You’ll know it’s done when you see a message telling you that the Pod installation [is] complete!
Nuke tutorial - Pod Installation

If you still have Far Out Photos.xcodeproj open, you’ll need to close it and then open Far Out Photos.xcworkspace from now on, instead. CocoaPods uses Xcode workspaces to add third party projects to your own.

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

Basic Nuke Usage

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

Most of your work in this Nuke tutorial will be focused on PhotoGalleryViewController.swift. Go ahead and open it, and add the following to the top of the file:

import Nuke

If you’re going to use Nuke, it would be helpful to import it.

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 it assigns it to the image in the collection view cell, blocking the main thread in the 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.

Holy cow, that was easy!

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

Nuke tutorial - Smooth Scrolling

Image Loading Options

That’s a great start, but there’s something very important that’s still missing. The app doesn’t give the user visual feedback that images are coming. Instead, if users scroll fast enough, they will 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 structure called ImageLoadingOptions, which allows you to change how images are presented, including setting a placeholder image.

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

// This line doesn't change
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, set the placeholder image 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, using the options you just configured.

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

Nuke tutorial - Placeholder Fade-in

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

What About Memory?

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

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. 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. It’s over a gigabyte of data 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 are still full-sized images and stored completely in memory. That’s not good.

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

What to do?

Maybe Nuke can help?

Yes. Nuke can help!

So far, you’ve been passing loadImage a URL. Those methods also have the option to accept an ImageRequest.

ImageRequest can define a target image size, and Nuke will automatically resize the downloaded image for you before assigning it to the image view.

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

// 1
let request = ImageRequest(
  url: url, 
  targetSize: CGSize(width: pixelSize, height: pixelSize), 
  contentMode: .aspectFill)

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

With this code, you:

  1. Create an ImageRequest for the desired image URL, with a target size of pixelSize x pixelSize, and the content mode set to fill the entire area while keeping the original aspect ratio.
  2. Have Nuke load the image based on this image request using the options you previously set into the cell’s image view.

So what’s pixelSize?

Nuke resizes images based on pixels, instead of points. At the top of the class, just below the definition of the cellSize property, add the following computed property:

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

This computed property takes the cellSize property, which is in points, and multiplies it by the device’s scale, which gives you pixels.

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

Wow! The VM: CG raster data is now under 30MB! You’re unlikely to experience a memory usage crash over 30MB. :]

Advanced Nuking

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(_:_:_) function each time.

Nuke has a better way to do this. In Nuke, you can define a single ImageLoadingOptions, which can serve as a default 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: success, failure and the placeholder.
  2. Set the default placeholder image.
  3. Set the default image to display when the there’s an error.
  4. Define the default transition from placeholder to another image.

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

First, remove the following lines:

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

You won’t need this anymore because you defined default options. Then, you need to change the call to loadImage(_:_:_:) 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.

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

Loading images without a view

OK, you need to solve another problem.

Currently, when you tap on an image in the gallery, the image is fetched on the main thread. This, as you already know, blocks the UI from refreshing.

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:

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

To fix this problem, you’re going to use — wait for it — Nuke. However, you won’t be using loadImage(_:_:). To understand why this method won’t work, take a look at the code in collectionView(_:didSelectItemAt:).

guard let photoViewController = PhotoViewController.instantiate() else {
  return
}

...

navigationController?.pushViewController(photoViewController, animated: true)

The detail view controller is instantiated and then pushed onto the navigation stack. If you were to try passing PhotoViewController‘s image view property to loadImage(_:_:_:), the app would crash. The image view property doesn’t exist at the time of the call.

To get around this, Nuke has a way to request an image without the need for an image view. Instead, you can define how it behaves while the image is loading, and what happens when it finishes.

At the bottom of PhotoGalleryViewController.swift, find collectionView(_:didSelectItemAt:) and delete everything after the guard statement.

Now, in its place, add the following code:

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

// 2
ImagePipeline.shared.loadImage(
  // 3
  with: photoURLs[indexPath.row],
  
  // 4
  progress: nil) { response, err in // 5
    if err != nil {
      // 6
    photoViewController.image = ImageLoadingOptions.shared.failureImage
    photoViewController.contentMode = .scaleAspectFit
  } else {
    // 7
    photoViewController.image = response?.image
    photoViewController.contentMode = .scaleAspectFill
  }
}

// 8
navigationController?.pushViewController(photoViewController, animated: true)

In this code, you:

  1. Set the placeholder image and content mode.
  2. Call loadImage(_:_:_:) on the ImagePipeline singleton.
  3. Pass in the appropriate photo URL.
  4. Set the progress handler to nil.
  5. Create a completion handler.
  6. Set the image to the appropriate failure image, if there was an error.
  7. Set the image to the downloaded photo, if everything was successful.
  8. Finally, push photoViewController onto the navigation stack.

Before you can build and run the app, you’ll need to take care of some errors that this newly added code introduced. PhotoViewController doesn’t have a contentMode property.

Open PhotoViewController.swift and find the declaration of the image property. Delete it and replace it with this code:

var image: UIImage? {
  didSet {
    imageView?.image = image
  }
}
  
var contentMode: UIView.ContentMode = .scaleAspectFill {
  didSet {
    imageView?.contentMode = contentMode
  }
}

This code allows you to change the UIImage and the UIView.ContentMode of the PhotoViewController after it has been created, before all of the views have been loaded.

Finally, in the same file, add the following line to the end of viewDidLoad(_:):

imageView?.contentMode = contentMode

This ensures that if the contentMode is set before the views have loaded, it will still be used.

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

No more UI freezing!

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, you can 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.razeware.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!
  2. Create a new data cache.
  3. Set the cache size limit to 200 MB (since there are 1024 bytes per kilobyte and 1024 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. Behind the scenes, however, 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()
}

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!

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

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

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

Average Rating

5/5

Add a rating for this content

2 ratings

Contributors

Comments