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.

Measuring Time

Now, you might be thinking this whole HEIC thing isn’t very impressive. The only thing clear at the moment is that compressing an image using HEIC is slow. Well, next you’ll see how much smaller a HEIC file is.

Included in the project is a helper file called Data+Additions.swift, which contains a computed property that pretty-prints the size of a Data object. This property uses the Foundation framework’s handy ByteCountFormatter to format the byte size.

In MainViewController.swift, replace TODO: Add image size here… inside compressJPGImage(with:) with:

self.jpgSizeLabel.text = data.prettySize

Like the JPEG method, replace the TODO: Add image size here… inside compressHEICImage(with:) with:

self.heicSizeLabel.text = data.prettySize

This will update the size label to reflect each image’s size.

Build and run. You should see right away how much more space you’ll save using HEIC, which is much more useful.

The last element to consider when choosing between HEIC and JPEG is time. The time it takes to compress is a key piece of data to consider. If your app needs speed over space, then HEIC might not be the best option for you.

At the top of MainViewController.swift add the following:

private let numberFormatter = NumberFormatter()

Formatters help make numbers more readable. This formatter will make it easier to read precise time intervals.

At the bottom of viewDidLoad(), right before updateImages(), add the following code:

numberFormatter.maximumSignificantDigits = 1
numberFormatter.maximumFractionDigits = 3

This configures the formatter to limit its output, as the difference between the two compression methods is noticeable. If the two were closer, then a higher level of precision would be beneficial.

Add the following method after resetLabels():

private func elapsedTime(from startDate: Date) -> String? {
  let endDate = Date()
  let interval = endDate.timeIntervalSince(startDate)
  let intervalNumber = NSNumber(value: interval)
  
  return numberFormatter.string(from: intervalNumber)
}

This method uses the formatter you declared earlier. It takes in a start date, then calculates the duration based on that and returns an optional string using the number formatter.

Inside compressJPGImage(with:) add this to the top of the method:

let startDate = Date()

Next, replace the TODO: Add compression time here… inside compressJPGImage(with:):

if let time = self.elapsedTime(from: startDate) {
  self.jpgTimeLabel.text = "\(time) s"
}

The start date gets recorded right away, ensuring that all parts of the method contribute to the calculated duration. The time label is then set as soon as the decoding finishes on the main queue.

Like before, you’ll need to add the accompanying logic for the HEIC image compression method. Add this to the top of compressHEICImage(with:):

let startDate = Date()

And replace the TODO: Add compression time here… with the following:

if let time = self.elapsedTime(from: startDate) {
  self.heicTimeLabel.text = "\(time) s"
}

When typing or copying similar code like this, make sure you’re setting the correct label. Notice this heicTimeLabel is set in the HEIC method and the jpgTimeLabel for the JPG method.

Build and run.

Now you can make a fully informed decision. JPG compression is very fast at the cost of a larger image. Conversely, the HEIC image is smaller but its compression is slower.

Being aware of your user’s device is a good thing. Since HEIC takes longer you might defer saving space when the battery is low. You could also check the device’s free storage space. If the disk is over 75 percent full, always opt for the HEIC image compression.

Sharing HEIC

One final thing to consider is sharing an HEIC image. The JPEG compression algorithm has been the standard on the web for many years, but the flexibility and space saving that HEIC has to offer is impressive.

Many well designed sites already compress their content with JPEG. If a site’s files are already small, then saving 50 percent isn’t as enticing. But saving half of the space of high quality iPhone photos is much more meaningful.

While it might not be the right time to go all in on HEIC on the web, some sites offer support to upload HEIC photos. Within the Apple ecosystem, other apps shouldn’t have a problem handling HEIC content. When sharing content, it’d be beneficial to offer the choice of what format to share.

Add the following method below updateImages():

private func shareImage(_ image: UIImage) {
  let avc = UIActivityViewController(
    activityItems: [image],
    applicationActivities: nil
  )
  present(avc, animated: true)
}

This method shares an image compressed in either format. The UIImage class takes care of handling its underlying format so you don’t have to.

To use this, add to following to shareButtonPressed() in MainViewController.swift:

let avc = UIAlertController(
  title: "Share",
  message: "How would you like to share?",
  preferredStyle: .alert
)

if let jpgImage = jpgImageView.image {
  avc.addAction(UIAlertAction(title: "JPG", style: .default) { _ in
    self.shareImage(jpgImage)
  })
}

if let heicImage = heicImageView.image {
  avc.addAction(UIAlertAction(title: "HEIC", style: .default) { _ in
    self.shareImage(heicImage)
  })
}

avc.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))

present(avc, animated: true)

This sets up the simple sharing feature for this app, as there are already the two compressed images in the image views. For a real world app that stores all images as HEIC you’d need to convert the image to JPEG before sharing.

One last quality of life feature to add to the example app is to prevent sharing when no images are available. The HEIC image compression might fail or take longer for a larger file. During this time it’d be beneficial to disable the share button to avoid confusion and safeguard a good user experience.

Inside updateImages() add:

navigationItem.leftBarButtonItem?.isEnabled = false

This line disables the share button before the compression gets kicked off.

Next, replace each occurrence of TODO: Disable share button here… with:

self.navigationItem.leftBarButtonItem?.isEnabled = true

As each compression finishes, the share button is re-enabled. You’ll see only the JPG option in the alert if the HEIC image compression is still processing. For your app, it might make more sense to wait until both options are available.

Build and run.

Congratulations, the example app is complete!

Where to Go From Here

You should now have a working knowledge of HEIC, its benefits and its weaknesses. There are many uses cases for HEIC, and it’ll only gain in popularity over time.

To recap the benefits of HEIC:

  • 50 percent smaller files sizes compared to JPEG.
  • Contain many image items.
  • Image derivations, non destructive edits.
  • Image sequences, like Live Photos.
  • Auxiliary image items for storing depth or HDR data.
  • Image metadata like location or camera information.

You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.

For more information about HEIC from Apple take a look at the WWDC session on HEIF and HEVC from 2017.

I hope you enjoyed this tutorial! If you have any questions or comments, please join the discussion below.