Picking a Photo in iOS 14

If your app puts up an interface where the user gets to choose a photo from the photos library, you’re probably familiar with UIImagePickerController. Indeed, you’re probably all too familiar with it.

UIImagePickerController is a remarkably clunky, aged piece of interface, both from the user point of view and with regard to its programming API. Here are some of its not-so-great features:

  • The UIImagePickerController view interface pretends to be a sort of reduction of the Photos app interface; but over the years, the Photos app has evolved, and UIImagePickerController has not always kept up. In iOS 13, for example, UIImagePickerController still displays a Moments interface that the Photos app itself no longer possesses.

  • Some of the UIImagePickerController options are effectively a dead letter. For instance, the .savedPhotosAlbum source type does not behave as advertised. In iOS 14, for example, .savedPhotosAlbum results in exactly the same interface as .photoLibrary, and there is little or no point in using it.

  • The UIImagePickerController interface provides no way to allow the user to select multiple photos.

  • UIImagePickerController provides an optional editing interface. But all it lets the user do to an image is to crop the chosen photo, and only to a fixed square shape — and who would ever want to do that?

  • The delegate method, where you (the programmer) retrieve the user’s chosen photo, hands you a clunky dictionary, any of whose keys might or might not be present. This requires you to do an elaborate dance in order to work out what’s happened. I’ll demonstrate the dance in the next section.

New in iOS 14, this entire use of UIImagePickerController is deprecated at long last. The picker is still available for letting the user capture an image or video with the camera. But if the goal is to let the user choose a photo from the library, you’re now supposed to use a different class, PHPickerViewController, provided by the PhotosUI framework (import PhotosUI). And you should use it!

In this article, I’ll introduce the new photo picker and talk about the main changes you’ll need to make in your approach in order to adopt it. But first, let’s refresh our memories about UIImagePickerController.

The UIImagePickerController Dance

If you’ve been using UIImagePickerController, your code probably starts something like this:

  1. Make sure the source type you intend to use, which in this case would be .photoLibrary, is available.

  2. Create the picker controller and set the source type.

  3. Specify the types of media you want to display (such as image or movie) and whether the user is to be permitted to edit the chosen media item.

  4. Set a delegate (adopting UIImagePickerControllerDelegate and UINavigationControllerDelegate).

  5. Present the picker.

Here’s a typical example:

let type = UIImagePickerController.SourceType.photoLibrary
guard UIImagePickerController.isSourceTypeAvailable(type) else { return }
let picker = UIImagePickerController()
picker.sourceType = type
picker.mediaTypes = [kUTTypeImage as String]
picker.delegate = self
self.present(picker, animated: true)

Your code comes to an end for now. Then, when the user actually chooses a photo, your delegate is called back:

func imagePickerController(_ picker: UIImagePickerController,
    didFinishPickingMediaWithInfo 
    info: [UIImagePickerController.InfoKey : Any]) { 
        // ...
}

What arrives into this delegate method is a dictionary (info), and what’s in that dictionary depends on what the user did. There are a lot of possibilities to cope with. But in the simplest case, where the user is allowed to pick only images and cannot crop the image before passing it on to you, you are guaranteed that you’ll get the following:

  • info[.originalImage]: The value is an Optional UIImage.

  • info[.imageURL]: The value is an Optional file URL where the image has been copied to disk.

So you can use either of those, whichever suits your needs.

You also have to dismiss the picker; it doesn’t go away by itself. Note that there is a second delegate callback method, imagePickerControllerDidCancel(_:), but there is usually no need to implement it, because if you don’t, then when the user taps the Cancel button, the picker does dismiss itself, which is usually what you want.

Getting Started with PHPickerViewController

All right then! That entire use of UIImagePickerController, as I’ve said, is now deprecated. Going forward, you’re supposed to use PHPickerViewController instead.

Here are the basic steps for getting started with a PHPickerViewController:

  1. Make a PHPickerConfiguration object.

  2. Set the configuration object’s properties as desired. In particular:

    • The filter limits what kinds of asset will be displayed.

    • The selectionLimit is the maximum number of items the user can choose. If it is not 1 (the default), an Add button will appear in the interface to let the user construct a multiple selection. A selectionLimit of 0 signifies an unlimited multiple selection.

  3. Using the configuration object, instantiate the picker.

  4. Give the picker a delegate (adopting PHPickerViewControllerDelegate).

  5. Present the picker.

Here’s an example:

var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
self.present(picker, animated: true)

That’s both simpler and more powerful than what the UIImagePickerController does. You can specify that a multiple selection is possible! There is no option to make the image editable, but that was always a silly feature and likely won’t be missed.

The Picker Delegate

The PHPickerViewController delegate callback is where things really start to get interesting. There’s just one method, which will be called whether the user chooses a photo or taps the Cancel button. So you need to dismiss the picker under all circumstances:

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    picker.dismiss(animated:true) {
        // ...
    }
}

As you can see from the declaration, you are being handed, under the name results, an array of PHPickerResult objects. If the results array is not empty, you know that the user has chosen one or more photos.

However, a PHPickerResult doesn’t actually contain any photo data! It’s a simple, lightweight object containing an NSItemProvider (its itemProvider). The NSItemProvider will hand you the data, but only if you ask for it. This is a delightful architecture, because you receive the data on demand — and if you don’t want any data just now, you don’t have to demand any.

I’ll demonstrate. Let’s say that the user chose one photo, and that you do want the image from that photo. Now what? Well, UIImage is a type that conforms to NSItemProviderReading. So it can be supplied directly through the item provider. To ask for the UIImage, call loadObject(ofClass:completionHandler:).

Keep in mind that this method is slow. Loading the image and returning it takes time. That’s why there’s a completion handler; you’re going to be called back asynchronously. Not only that, but you’re going to be called back on a background queue; so if you intend to do anything with the resulting image, such as display it in your interface, you’re probably going to want to step out to the main thread first.

Here’s a typical approach (this code is intended to go into the spot where I put a comment in the preceding code, namely the completion handler of the picker dismissal):

guard let result = results.first else { return }
let prov = result.itemProvider
prov.loadObject(ofClass: UIImage.self) { imageMaybe, errorMaybe in
    if let image = imageMaybe as? UIImage {
        DispatchQueue.main.async {
            // do something with the image
        }
    }
}

Notice that the first parameter, which I’ve called imageMaybe, is an Optional. It might or might not be nil, and if it isn’t nil it might or might not be a UIImage — even though we specifically asked for a UIImage! So we have to unwrap it and cast it down to a UIImage safely before using it.

Receiving a File

In the preceding example, we retrieved the image directly as a UIImage object. But there’s another possibility. Recall that the UIImagePickerController delegate method also provides the URL of a file where the image is saved to disk. When using PHPickerViewController, you might prefer something like that, rather than being handed a UIImage object.

Why would a file URL be better than a UIImage object? Well, for one thing, keep in mind that photo images are big. Available memory isn’t limitless, so if at all possible, you probably don’t want to hold an entire original photo UIImage in memory; and in many cases, you won’t need to.

For example, let’s say your goal is to display this image in your app’s interface. Well, the screen might be considerably smaller than the dimensions of the original image. So all you really need is a thumbnail — that is, a smaller version of the image. The ImageIO framework will gladly load an image from disk for you while reducing it to a specified thumbnail size at the same time. So if you can get the user’s chosen image to be sent to disk rather than being handed to you as a UIImage, the full-size image will never actually have to be held in your app’s own memory.

The way to get an NSItemProvider to save its data to disk, rather than handing it to you directly, is to call loadFileRepresentation. Let’s suppose you want the image data as a JPEG file. You will need to specify this using a uniform type identifier (UTI). New in iOS 14, there’s a UTType class that lets you do that; the old UTI CFString constants, such as kUTTypeJPEG, are deprecated. So you would say:

let jpeg = UTType.jpeg.identifier
let prov = result.itemProvider
prov.loadFileRepresentation(forTypeIdentifier: jpeg) { urlMaybe, errorMaybe in
    guard let url = urlMaybe else { return }
    // and now url is the file URL of our JPEG
}

One thing to watch out for is that this URL is temporary. This means it will become invalid as soon as the completion handler exits. If you intend to do something with the data on disk, you need to do it immediately! If that’s not possible, tell the FileManager to copy the file somewhere else so that it will persist long enough to work with it.

Permission

An important consideration when using either UIImagePickerController or PHPickerViewController is whether you’re going to need user authorization to access the photo library. Things have changed over the years in this regard, and they’ve changed again in iOS 14, so let’s delve into the matter now.

There was a time when you couldn’t use UIImagePickerController at all without user authorization. But those days are long gone; Apple saw that this was an erroneous approach. An image is just an image, after all. So nowadays the picker is happy to hand you a UIImage derived from a photo stored in the photo library, even if you don’t have authorization to access the photo library directly.

On the other hand, if your goal is to return to the photo library later to directly access the actual asset corresponding to the user’s choice, then you’re going to need a PHAsset, and you won’t be able to get it without user authorization.

This, it turns out, is yet another reason for switching from UIImagePickerController to PHPickerViewController. That’s because UIImagePickerController copes very poorly with user authorization in iOS 14, as I will now explain.

Obtaining Authorization

In iOS 14, there’s a new kind of photo library authorization: limited authorization. The idea here is that the user can explicitly list the photos to which your app will have access. Limited authorization is a new kind of authorization status; in addition to .denied and .authorized and so forth, there is now .limited. How is your code going to cope with this?

Well, Apple doesn’t want to break your existing code due to the existence of a new authorization status for which your code is unprepared. Therefore, if you call the long-standing PHPhotoLibrary class methods authorizationStatus() and requestAuthorization(_:), if the user has granted .limited authorization, the runtime will just lie to you and say that your status is .authorized instead. You’re using old methods so you get the old answer.

If, on the other hand, you want to be able to distinguish between .limited authorization and being fully .authorized — and you probably do want that — then you need to use a different pair of methods, new in iOS 14: authorizationStatus(for:) and requestAuthorization(for:handler:). The for: parameter is .addOnly or .readWrite, drawing yet another new distinction regarding the type of authorization you’re talking about:

  • .addOnly: All you’re allowed to do is add a photo to the library.
  • .readWrite: You can fetch information from the library and can modify the library in other ways.

This is a nice improvement because it means that, new in iOS 14, you can explicitly ask for add-only authorization, which was previously impossible. In iOS 13 and before, the only way to learn whether you could add something to the photo library was to try it.

The distinction between .addOnly and .readWrite, however, is not germane to the current discussion. We need to specify .readWrite! The important point is that your use of these new iOS 14 methods tells the runtime that you are iOS 14-savvy, and the runtime then distinguishes .limited from .authorized for you.

The Picker and Limited Access

Never mind now whether you are calling the old or the new authorization methods. The important thing is that, whether your code knows it or not, you might be given only limited authorization. If that happens, the UIImagePickerController can break, as I will now demonstrate.

Let’s suppose that your app obtains authorization or ensures that it already has authorization, and then proceeds to put up the UIImagePickerController. The user chooses a photo, and the delegate method is called. And let’s say that your goal is now is to retrieve the PHAsset associated with this image; that is why you obtained authorization in the first place. You do that by using this key in the info dictionary:

  • info[.phAsset]: The value is an Optional PHAsset.

It turns out, however, that if your authorization status is actually .limited, regardless of whether you are consciously aware of that fact, the Optional PHAsset provided by info[.phAsset] will always be nil — even if the photo chosen by the user is one of the photos that you are authorized to access! Basically, limited access has killed the ability of your app to reach the photo library at all. You’ve been told that you are .authorized, but the runtime behaves as if you were not .authorized. What a mess!

The solution is to switch to PHPickerViewController. It makes all the necessary distinctions correctly. Let’s demonstrate.

First of all, after you obtain authorization or determine that you already have it, if you are going to need a PHAsset, you need to start by initializing the PHPickerConfiguration object with a different initializer:

var config = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())

The outcome is that when the delegate method is called, the PHPickerResult’s assetIdentifier is a non-nil String identifying the PHAsset. To put it another way: if you created the PHPickerConfiguration object by saying simply PHPickerConfiguration(), the PHPickerResult’s assetIdentifier will always be nil and you won’t be able to learn what asset corresponds to the user’s chosen photo. So if your assetIdentifier is coming out nil and that’s not what you were expecting, that’s probably the reason.

Okay, so presume you have a non-nil PHPickerResult assetIdentifier. You can now fetch the actual asset from the photo library in the usual way:

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    picker.dismiss(animated:true) {
        guard let result = results.first else { return }
        if let ident = result.assetIdentifier {
            let result = PHAsset.fetchAssets(withLocalIdentifiers: [ident], options: nil)
            if let asset = result.firstObject {
                // ...
            }
        }
    }
}

And here’s the delightful part: this works just as you would expect! If you only have .limited access, and if this is not one of the assets to which the user has explicitly granted you access, then the call to fetchAssets will return an empty PHFetchResult, and asset will be nil. But if it is one of the assets to which you have access, asset will be an actual PHAsset, and you can go ahead and use it however you like.

What the User Chose

That just about completes my little survey of PHPickerViewController. But before I go, here’s one final tip about using it.

If your configuration’s filter allows the user to choose any type of asset — a video, a live photo, or a normal photo — you’re going to want to distinguish what type the user actually chose. This turns out to be no simple matter.

After a great deal of futzing around, I’ve settled on a formula like this:

// prov is the PHPickerResult's itemProvider
if prov.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
    // it's a video
} else if prov.canLoadObject(ofClass: PHLivePhoto.self) {
    // it's a live photo
} else if prov.canLoadObject(ofClass: UIImage.self) {
    // it's a photo
}

In that code, the order of the tests is deliberate. A live photo can be supplied in a simple UIImage representation, so if we test for images before live photos, we won’t learn that the result is a live photo.

You Might Also Like…

Finishing Touches: Haptics

We are probably all so accustomed by now to haptic feedback on our iPhones that we hardly give it a thought. Here are some examples: On the home screen, long press on the background, and the screen enters “jiggly mode” — with a dull thud. On the home screen, long press an app or a …

Finishing Touches: Haptics Read More »

    Sign Up

    Get more articles and exclusive content that takes your iOS skills to the next level.