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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Nuke Tutorial for iOS: Getting Started
30 mins
- Getting Started
- Setting up Nuke
- Installing with Swift Package Manager
- Using Nuke: Basic Mode
- Setting Loading Options
- Monitoring Memory Usage
- Using Instruments
- Advanced Nuking
- Loading with Requests
- Optimizing Code
- Using ImagePipeline to Load Images
- Caching Images
- Combining with Combine
- Setting up ImagePublisher
- Using ImagePublisher
- Chaining Requests
- Cleaning the Subscriber
- Where to Go From Here?
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.
Using Instruments
To see how bad it is, run the app again, and then do the following:
- Select the Debug navigator in the Navigator panel.
- Then select Memory under the list of debug gauges.
- Click Profile in Instruments.
- Finally, click Restart in the dialog that appears.
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!
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:
-
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. -
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:
- 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. - 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.
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:
- Define the default
contentMode
for each type of image loading result: success, failure and the placeholder. - Set the default placeholder image.
- Set the default image to display when there’s an error.
- 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.
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:
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:
- Set the placeholder image and content mode.
- Call
loadImage(with:)
on theImagePipeline
singleton. - Pass in the appropriate photo URL.
- Provide a completion handler. The handler has a parameter of an enum type
Result<ImageResponse, Error>
. - The
response
can have only two values:.success
with an associated value of typeImageResponse
, or.failure
with an associated value of typeError
. So a switch statement will work best to check for both possible values. - In the failure case, set the image to the appropriate failure image.
- For success, set the image to the downloaded photo.
There! It’s time. Build and run and tap the Eagle Nebula photo once again.
No more UI freezing! Great work.