Working with images in iOS projects is not rare. There are various operations one could perform to an image, and sometimes such an operation is to create a thumbnail, meaning a smaller version, of the original image.
At first sight it might look like a complicated process involving a series of transformations that the image has to go through. That’s not true, though; the Core Graphics framework is here to support us, providing us with all the -really few- APIs we need to carry out such a task.
With that said, let’s get straight into the point and let’s find out what it takes in order to create image thumbnails easily. Once that’s out of the way, we’ll put it in motion in a pretty simple demo application.
Creating an image thumbnail
All we’re going to do is to implement a method that will be performing the thumbnail creation. You can define that method anywhere that’s convenient to you, but I strongly believe that the best place to keep it is in a UIImage
extension. That way, it will be always available to all images.
So, let’s get started with that:
1 2 3 4 5 6 7 8 9 10 |
extension UIImage { func createThumbnail( scaleTo scaleValue: CGFloat, completion: @escaping (_ thumbnail: UIImage?) -> Void ) { } } |
There are two things to note here:
- In this particular demonstration the desired size of the thumbnail will be provided as a percentage of the original image size. The
scaleValue
parameter will get a value from 0 to 1, indicating the final size percentage. Of course, you could change that, or create method overloads, that would accept a downsize factor differently; for instance, you could provide an exact thumbnail size. - Depending on the size of the original image, the thumbnail creation might freeze the user interface (UI) for a few moments until the process is complete. To avoid that, I would recommend to perform everything in a background thread so users do not experience any sort of disturbance. I’ll apply that logic right next, and the thumbnail image will be handed over back to the caller through the completion handler that you see above. Note that later on I’ll provide you with an async/await alternative as well.
Now, the first thing in the method’s body is to define a background queue where we’ll add everything else that will come next:
1 2 3 4 5 |
DispatchQueue.global(qos: .background).async { } |
There is a specific function in Core Graphics framework that does the actual thumbnail creation, called CGImageSourceCreateThumbnailAtIndex(_:_:_:)
. Before we become capable of using it, it’s required to perform some prior steps in order to prepare the values that will be given as arguments to it.
Getting a CGImageSource object from the original image
The first argument the above function accepts is a CGImageSource
object. Such an object can extract image information from image data or from a URL. In this post we’ll go with the first option and use the original image’s data.
There is another Core Graphics function that can provide us with a CGImageSource
object which you’ll see next, but in order to be able to use it it’s mandatory to get a Data
object from the UIImage
that we’re working on. So, let’s do that first:
1 2 3 4 5 6 |
guard let imageData = self.jpegData(compressionQuality: 1) else { completion(nil) return } |
We’re getting the image data using the jpegData(compressionQuality:)
instance method from the UIImage
class here, but you could use the pngData()
method as well. Notice that if the above won’t work and imageData
won’t get a value, then there is no reason to continue; we call the completion handler passing nil
and exit the method.
We can now go ahead and initialize a CGImageSource
object using the CGImageSourceCreateWithData(_:_:)
function from Core Graphics. The first argument is the image data, the second a dictionary of options which we’ll leave empty:
1 2 3 4 5 6 |
guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) else { completion(nil) return } |
As it’s not certain if a CGImageSource
object will be created or not, a guard let
statement is necessary here as well. In case of a nil
result we call the completion handler passing nil
and exit the method once again.
Note that imageData
is a Data
object, but the above function expects for a CFData
object instead. Casting to CFData
with the as CFData
suffix is unavoidable.
Specifying thumbnail options
Along with the image source, the other argument that’s necessary to prepare for the CGImageSourceCreateThumbnailAtIndex(_:_:_:)
function is a dictionary with a few values regarding the thumbnail.
The first and probably the most important one is about the thumbnail size. As said already, we’ll calculate and set a percentage of the original image size using the scaleValue
parameter. But there is an important detail to keep in mind here:
Thumbnail size does not define the width and height of the final image! Thumbnail size is the maximum size that the biggest dimension of the image will get, either that’s the width or the height. The size of the other dimension will be calculated accordingly automatically.
Having that in mind, here’s how we’ll calculate the thumbnail size:
1 2 3 |
let thumbnailSize = max(self.size.width, self.size.height) * scaleValue |
At first we calculate the value of the maximum dimension of the original image, and then we scale down by multiplying with the scaleValue
.
Besides the thumbnail size we’ll also add two more key-value pairs to the dictionary that we’ll define next:
- A flag to indicate that the thumbnail image should rotate according to original image’s orientation and respect the original aspect ratio.
- A flag that must be mandatorily present to the dictionary (a few more words about that next).
Here’s the options dictionary including all the above:
1 2 3 4 5 6 7 |
let options = [ kCGImageSourceThumbnailMaxPixelSize: thumbnailSize, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailFromImageAlways: true ] as CFDictionary |
Note that casting to CFDictionary
is mandatory; the CGImageSourceCreateThumbnailAtIndex(_:_:_:)
function expects for such an object.
The last key in the options
dictionary is the mandatory one, but it can be replaced by the kCGImageSourceCreateThumbnailFromImageIfAbsent
key as well. Using the current key, the thumbnail will be always created based on the original image. With the latter, the CGImageSourceCreateThumbnailAtIndex(_:_:_:)
function will first check if a thumbnail image exists in the image source object or not, and it will create it if only it’s missing (note that an image source object can contain image data from more than one images).
There are more options you could specify in the above dictionary, but those here should be always present in my opinion. You can find more about available options here. Note that the entry regarding the thumbnail size can be omitted, but then guess what; the thumbnail will have the same size as the original image.
Generating the thumbnail image
With the image source and the options dictionary being ready, it’s time to generate the thumbnail image. Here’s how to do it:
1 2 3 4 5 6 |
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) else { completion(nil) return } |
The generated thumbnail is a CGImage
object, not a UIImage
. If it cannot be created then we provide nil
to the completion handler and leave the method.
See that the first argument above is the imageSource
object created in a previous step. The last argument is the dictionary with the options. As far as it regards the second parameter value, it indicates the index of the image to create thumbnail for in the given image source. An image source might contain data regarding a series of images, not just one. For example, a PDF file. In the simple example demonstrated here, as well as in the majority of the cases where you’ll need to create thumbnails, image source will contain a single image’s data. So, the index is zero (0).
There is one last step, which is to create a UIImage
object and pass it to the completion handler. We’ll do that in one line like so:
1 2 3 |
completion(UIImage(cgImage: thumbnail)) |
Here is everything presented step by step so far in a single code snippet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
extension UIImage { func createThumbnail( scaleTo scaleValue: CGFloat, completion: @escaping (_ thumbnail: UIImage?) -> Void ) { DispatchQueue.global(qos: .background).async { // Get the image data. guard let imageData = self.jpegData(compressionQuality: 1) else { completion(nil) return } // Create an image source object using the image data. guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) else { completion(nil) return } // Specify the thumbnail size. let thumbnailSize = max(self.size.width, self.size.height) * scaleValue // Create a dictionary with the minimum recommended options. let options = [ kCGImageSourceThumbnailMaxPixelSize: thumbnailSize, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceCreateThumbnailFromImageAlways: true ] as CFDictionary // Generate the thumbnail. guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options) else { completion(nil) return } // Create a UIImage representation of the thumbnail // and pass it to the completion handler. completion(UIImage(cgImage: thumbnail)) } } } |
The async/await alternative
In the async/await era and the new concurrency APIs that Apple provided us with, the above method might look a bit outdated because one of its arguments is a completion handler. Note, however, that not everyone uses async/await -yet-, so what is presented above covers that part of developers.
If you’re an async/await fan on the other hand, then it’s easy to use the thumbnail creating method without the completion handler. To achieve that, simply add the following method as well to the UIImage
extension:
1 2 3 4 5 6 7 8 9 |
func createThumbnail(scaleTo scaleValue: CGFloat) async -> UIImage? { await withUnsafeContinuation({ continuation in createThumbnail(scaleTo: scaleValue) { thumbnail in continuation.resume(returning: thumbnail) } }) } |
See that this one is marked as async
and returns an optional UIImage
object; the thumbnail image or nil
if it was not generated for some reason.
In the method’s body a call to withUnsafeContinuation(_:)
function invokes the createThumbnail(scaleTo:completion:)
method. When the completion handler will be called containing either the thumbnail image or nil
, then the execution is resumed returning the contents of the completion handler from the method.
Demonstrating thumbnail creation
Time to see how all the above work, and in order to perform the demonstration we’ll use the following simple SwiftUI view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
struct ContentView: View { @State private var originalImage = UIImage(named: “demoImage”) ?? UIImage() @State private var thumbnailImage: UIImage? var displayedImage: UIImage { thumbnailImage ?? originalImage } var body: some View { VStack { Image(uiImage: displayedImage) .resizable() .scaledToFit() .frame(width: min(UIScreen.main.bounds.size.width, displayedImage.size.width)) Text(“Image Size: \(Int(displayedImage.size.width)) x \(Int(displayedImage.size.height))”) .padding() if thumbnailImage == nil { Button(“Create Thumbnail”) { } } } } } |
There are two UIImage
state properties in the view, the originalImage
and thumbnailImage
. The former loads an image from the assets catalog while the latter is initially nil
. In this one we’ll assign the thumbnail image after its creation.
Which image will be displayed is specified in the displayedImage
computed property. If thumbnailImage
is not nil
and contains an image, then this is what will be shown in the view. Otherwise, the original image is what we’ll see. Obviously, the original image is what will be displayed when the app is launched.
In the view’s body there is a VStack
container that contains:
- An Image view that displays the image determined in the
displayedImage
. - A Text view that shows the size of the displayed image (width and height).
- A button (visible only when showing the original image) which will trigger the creation of the thumbnail image.
What is missing in the above code is the button’s action. To generate the thumbnail we can use either the createThumbnail(scaleTo:completion:)
method with the completion handler, or the createThumbnail(scaleTo:)
async method.
Following the first approach, here’s how to create the thumbnail and keep in the thumbnailImage
property:
1 2 3 4 5 6 7 |
Button(“Create Thumbnail”) { originalImage.createThumbnail(scaleTo: 0.25) { thumbnail in thumbnailImage = thumbnail } } |
To use the async alternative, it’s necessary to call the createThumbnail(scaleTo:)
method in a Task
:
1 2 3 4 5 6 7 |
Button(“Create Thumbnail”) { Task { thumbnailImage = await originalImage.createThumbnail(scaleTo: 0.25) } } |
Both of the above are going to work just fine, and in both cases we set the thumbnail image’s size to 25% of the original one.
No matter what the path we’re going to take is, right next you can see everything in action:
Conclusion
In a nutshell, creating an image’s thumbnail is not much of a work, and the steps to get to it remain pretty much the same all the time. It’s just configuration details that might get changed, but that’s all to it. In this post I shared two different methods to fetch the thumbnail, one with the completion handler and one with the async/await alternative, so pick what you prefer the most.
Thank you for reading!