HEIC Image Compression for iOS

In this HEIC image compression tutorial, you’ll learn how to transform images into HEIC and JPEG formats, comparing their efficiency for optimum performance. By Ryan Ackermann.

Leave a rating/review
Download materials
Save for later
Share

In today’s modern world, photos and videos typically take up most of a mobile device’s disk space. Since Apple continues to invest time and money into the iPhone’s camera, this will continue to be the case for people using iOS devices. Higher quality photos means larger image data. There’s a reason why 4K cameras take up so much space!

With so much image data to store, there’s just so much you could do by increasing hardware storage size. In order to help minimize data footprint, various data compression algorithms were invented. There are many data compression algorithms and there are no perfect one-size fits all solutions. For images, Apple has adopted the HEIC Image Compression. You’ll learn all about this compression in this tutorial.

Formatting and HEIC Image Compression

The term JPEG is often used to describe an image’s file type. While the file’s extension, .jpg or .jpeg, can be misleading, JPEG is actually a compression format. The most common files types created by JPEG compression are JFIF or EXIFF.

HEIF, or High Efficiency Image File Format, is a new image file format that is better than its JPEG predecessor in many ways. Developed by MPEG in 2013, this format claims to save twice as much data as JPEG and supports many types of image data including:

  • Items
  • Sequences
  • Derivations
  • Metadata
  • Auxiliary image items

These data types make HEIF far more flexible than the data of a single image that JPEG can store. This makes practical use cases, like storing edits of an image, extremely efficient. You can also store image depth data recorded on the latest iPhone.

There are a few file extensions defined in the MPEG’s specification. For their HEIF files, Apple decided to use the .heic extension, which stands for High Efficiency Image Container. Their choice indicates the use of the HEVC codec, but Apple’s devices can also read files compressed by some of the other codecs as well.

Getting Started

To get started, click the Download Materials button at the top or bottom of this tutorial. Inside the zip file you’ll find two folders, Final and Starter. Open the Starter folder.

The project is a simple example app displaying two image views and a slider for adjusting the JPEG and HEIC image compression levels. Beside each image view are a couple labels for displaying information about the selected images, all of which form a functionless skeleton at the moment.

The goal of this app is to show the advantages of using HEIC vs JPEG by showing how long an image takes to compress and how much smaller an HEIC file is. It also shows how to share an HEIC file using a share sheet.

Saving As HEIC

With the starter project open, build and run to see the UI of the app in action.

The base app.

Before you start compressing images, you need to be able to select images. The default image by Jeremy Thomas on Unsplash is nice, but it’ll be even better to see how this works on your own content.

Inside MainViewController.swift, add the following to the bottom of the file:

extension MainViewController: UIImagePickerControllerDelegate, 
                              UINavigationControllerDelegate {
  func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
    // 1
    picker.dismiss(animated: true)
  }
  
  func imagePickerController(
    _ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo
    info: [UIImagePickerController.InfoKey : Any]
    ) {
    picker.dismiss(animated: true)
    
    // 2
    guard let image = info[.originalImage] as? UIImage else {
      return
    }
    
    // 3
    originalImage = image
    updateImages()
  }
  
}

This is a simple implementation of UIImagePickerControllerDelegate. Here you:

  1. Dismiss the picker when the cancel button gets pressed.
  2. Get the original image from the picker for the best results in this app.
  3. Store this image and update the image views.

For now updateImages() does nothing. Next, add these lines to the empty addButtonPressed():

let picker = UIImagePickerController()
picker.delegate = self

present(picker, animated: true)

This presents an image picker that gives users the opportunity to choose their own image. But, you still need to update the image views to get this working.

Replace the empty implementations of compressJPGImage(with:) and compressHEICImage(with:) with the followintg:

private func compressJPGImage(with quality: CGFloat) {
  jpgImageView.image = originalImage
}

private func compressHEICImage(with quality: CGFloat) {
  heicImageView.image = originalImage
}

Now both image views will display the selected image. The selected image is temporary but will verify that the image picker is working.

Now, build and run the app. Select an image to see if it appears in both image views.

With the images mocked out, you can move on to the compression slider. It doesn’t do anything yet, but eventually it’ll be able to change the compression strength of each type.

Compressing images on the simulator is much slower than on device. To work around this you need a conditional to determine how to read the slider’s value.

Start by adding the following at the top of MainViewController.swift, above originalImage:

private var previousQuality: Float = 0

This property will store the last slider value, which you’ll use later to limit the number of updates based on the slider’s value.

Next, add the following two methods at the end of the Actions section in MainViewController.swift:

@objc private func sliderEndedTouch() {
  updateImages()
}

@objc private func sliderDidChange() {
  let diff = abs(compressionSlider.value - previousQuality)
  
  guard diff > 0.1 else {
    return
  }
  
  previousQuality = compressionSlider.value
  
  updateImages()
}

Both of these methods update the images on screen. The only difference is the bottom method throttles the number of updates based on noticeable changes in the sliders value.

At the bottom of viewDidLoad() add the following:

compressionSlider.addTarget(
  self,
  action: #selector(sliderEndedTouch),
  for: [.touchUpInside, .touchUpOutside]
)

This registers a target action to the slider that update’s the images after interactions with the slider are complete.

With that hooked up, it’s finally time to begin compressing these images.

Add the following property at the top of MainViewController.swift:

private let compressionQueue = OperationQueue()

An operation queue is a way to offload heavy work, ensuring the rest of the app is responsive. Using a queue also provides the ability to cancel any active compression tasks. For this example, it makes sense to cancel current tasks before starting new ones.

Note: If you’d like to learn more about what operation queues have to offer, take a look at our video course.

Add the following line after the call to resetLabels() inside updateImages():

compressionQueue.cancelAllOperations()

This cancels any operations currently on the queue before adding new tasks. Without this step, you could set an image with the wrong compression quality in the view.

Next, replace the contents of compressJPGImage(with:) with the following:

// 1
jpgImageView.image = nil
jpgActivityIndicator.startAnimating()

// 2
compressionQueue.addOperation {
  // 3
  guard let data = self.originalImage.jpegData(compressionQuality: quality) else {
    return
  }
  
  // 4
  DispatchQueue.main.async {
    self.jpgImageView.image = UIImage(data: data)
    // TODO: Add image size here...
    // TODO: Add compression time here...
    // TODO: Disable share button here...
    
    UIView.animate(withDuration: 0.3) {
      self.jpgActivityIndicator.stopAnimating()
    }
  }
}

With the code above, you:

  1. Remove the old image and start the activity indicator.
  2. Add the compression task to the defined operation queue.
  3. Compress the original image using the quality parameter and convert it to Data.
  4. Create a UIImage from the compressed data and update the image view on the main thread. Remember that UI manipulation should always happen on the main thread. You’ll be adding a more code to this method soon.

That’s it for compressing an image using the JPEG codec. To add HEIC image compression, replace the contents of compressHEICImage(with:) with:

heicImageView.image = nil
heicActivityIndicator.startAnimating()

compressionQueue.addOperation {
  do {
    let data = try self.originalImage.heicData(compressionQuality: quality)
    
    DispatchQueue.main.async {
      self.heicImageView.image = UIImage(data: data)
      // TODO: Add image size here...
      // TODO: Add compression time here...
      // TODO: Disable share button here...
      
      UIView.animate(withDuration: 0.3) {
        self.heicActivityIndicator.stopAnimating()
      }
    }
  } catch {
    print("Error creating HEIC data: \(error.localizedDescription)")
  }
}

There’s only one difference with the HEIC image compression method. The image data is compressed in a helper method in UIImage+Additions.swift, which is currently empty.

Open UIImage+Additions.swift and you’ll find an empty implementation of heicData(compressionQuality:). Before adding the contents of the method, you’ll need a custom error type.

Add the following at the top of the extension:

enum HEICError: Error {
  case heicNotSupported
  case cgImageMissing
  case couldNotFinalize
}

This Error enum contains a few cases to account for the kinds of things that can go wrong when compressing an image with HEIC. Not all iOS devices can capture HEIC content, but most devices running iOS 11 or later can read and edit this content.

Replace the contents of heicData(compressionQuality:) with:

// 1
let data = NSMutableData()
guard let imageDestination =
  CGImageDestinationCreateWithData(
    data, AVFileType.heic as CFString, 1, nil
  )
  else {
    throw HEICError.heicNotSupported
}

// 2
guard let cgImage = self.cgImage else {
  throw HEICError.cgImageMissing
}

// 3
let options: NSDictionary = [
  kCGImageDestinationLossyCompressionQuality: compressionQuality
]

// 4
CGImageDestinationAddImage(imageDestination, cgImage, options)
guard CGImageDestinationFinalize(imageDestination) else {
  throw HEICError.couldNotFinalize
}

return data as Data

Time to break this down:

  • To begin, you need an empty data buffer. Additionally, you create a destination for the HEIC encoded content using CGImageDestinationCreateWithData(_:_:_:_:). This method is part of the Image I/O framework and acts as a sort of container that can have image data added and its properties updated before writing the image data. If there is a problem here, HEIC isn’t available on the device.
  • You need to ensure there is image data to work with.
  • The parameter passed into the method gets applied using the key kCGImageDestinationLossyCompressionQuality. You’re using the NSDictionary type since CoreGraphics requires it.
  • Finally, you apply the image data together with the options to the destination. CGImageDestinationFinalize(_:) finishes the HEIC image compression and returns true if it was successful.

Build and run. You should now see that the images will change based on the slider’s value. The bottom image should take longer to appear because there is more involved with the HEIC image compression due to how it saves more space on disk.