MacKuba

Kuba Suder's blog on Mac & iOS development

Photo library changes in iOS 14

Categories: Cocoa, iPhone Comments: 0 comments

I’m the kind of person who cares a lot about their digital privacy. It makes me very uncomfortable when I see ads on Facebook for something I opened on another site a moment ago, and I generally don’t like it when companies are learning more about me than they should, even if the effects of that tracking aren’t as obvious.

That’s why for example I’ve been trying to move away from Google services as much as possible (I use ProtonMail as my main email and Apple’s iWork for documents), I also started using Tresorit and iCloud1) for file sync instead of Dropbox. That’s also one of the reasons why I’ve always used some kind of ad & tracker blocker in my browsers – previously Ghostery, now I also use Brave and I’ve been experimenting with making my own ad blocker.

So it always makes me happy when Apple introduces another change to their OSes that limits the kinds of data that Mac and iOS apps can use without our permission. I especially liked:

  • when iOS 11 introduced the “While Using” option for location access that was non-optional for apps
  • the “Allow Once” option for location access in iOS 13
  • permissions to things like camera, microphone or screen recording on the Mac

This year Apple made another batch of changes that limit apps' access to data. The most interesting ones are the approximate location access and the limited photo library – in this post I’ll talk about the latter.


Most of us have thousands of photos on our phones, often going a few years back – after all, our iPhones are our primary cameras these days. These photos and videos capture everything we do, the places we go to, who we meet with and what we do together. They also include location info in their metadata. This is all possibly extremely sensitive data.

So far however if you wanted to upload a single photo or screenshot to e.g. Twitter or Facebook or send it to a friend through a messaging app, you had to grant them access to your whole photo library – it was all or nothing. And you could never be sure what they do with it – are they just looking at this single picture, or maybe looking through your whole 30 GB library for any interesting stuff they can find there, and uploading that to their servers? Hopefully they aren’t, but you just had to trust them on this.

Apple had previously provided a system image picker (UIImagePickerController) that lets the user choose a photo from their library and pass it to the app without giving it access to the library, as well as a way to save photos to the library without seeing what else is there (UIImageWriteToSavedPhotosAlbum()). However, for various reasons these don’t seem to be widely used in popular apps – most apps that do anything with photos currently ask for full read-write access to the whole library, just because they can.

So this year Apple is taking a bit of a carrot and stick approach: the carrot is a new improved system photo picker, while the stick is a new way for the user to only give the app access to selected photos.


PHPicker

PHPicker (not an actual name you can find in the docs, but a general name Apple uses for this new API) is a new system photo picker, a replacement for the old UIImagePickerController. The two most important differences are:

  • it has an integrated search, so it can help you find some specific photos that may not be recent
  • unlike UIImagePickerController it allows multiple selection

It also has an updated design, and while you’re scrolling the photo grid you can zoom in and out to see more or less photos at the same time:

How to use the picker

The API consists of a few types with the PHPicker* prefix, and most of them are surprisingly simple.

The main class that handles the picker screen is PHPickerViewController. It has a delegate protocol, PHPickerViewControllerDelegate, which you need to implement. You also need a PHPickerConfiguration object to pass it to the picker controller, in which you can set a few options for the picker.

You create a picker controller like this:

var config = PHPickerConfiguration()
// ...
let picker = PHPickerViewController(configuration: config)
picker.delegate = self

There are currently two options you can set in the picker configuration:

1) selectionLimit

This is the maximum number of items that the user can pick. The default is 1, and you can set it to some specific number, or to 0 to allow unlimited selection.

config.selectionLimit = 0

2) filter

The filter can be one of .images, .livePhotos, .videos, or a subset of those created using the .any(of:) helper:

config.filter = .images
config.filter = .any(of: [.images, .livePhotos])

Once the picker is configured, you can present it in the usual way:

present(picker, animated: true)

The last thing to do is to implement PHPickerViewControllerDelegate, which includes literally a single method: picker(_: didFinishPicking results:). This method is called with a list of one or more PHPickerResult objects in the response when the user confirms their selection. With single selection it returns immediately when the user taps a photo, and with multi-selection they need to confirm it with a toolbar button when they finish selecting.

The only part here that is not simple is that the photos are returned wrapped in NSItemProvider objects (used e.g. in the drag & drop API, or in some kinds of extensions). You need to get that item provider and first call canLoadObject(ofClass:) and then loadObject(ofClass:) (though I’m not 100% sure if the first is technically required).2)

You also need to dismiss the picker view – it doesn’t hide itself automatically:

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

    for result in results {
        let provider = result.itemProvider

        if provider.canLoadObject(ofClass: UIImage.self) {
            provider.loadObject(ofClass: UIImage.self) { image, error
                // ... save or display the image, if we got one
            }
        }
    }
}

Apple is expecting that most apps that only access the photo library to attach one or two pictures to a post will switch to this new system picker now. (The old UIImagePickerController is deprecated – that is, the class itself is not, but it’s only keeping the camera part of its functionality.)

And if they don’t like the carrot… well, then there’s still the stick.


Limited photo library

The stick is that there is now a standard way for the user to only grant an app access to a selected subset of photos (most likely just a few, since they need to manually tap each one). This is *not opt-in* for apps – it affects every app immediately, even those that have been built on older SDKs.

The way it works is that when the app tries to access the photo library (or explicitly asks for authorization), the user will now see a popup that looks like this:

The top option leads them to a selection dialog which is the same new picker you’ve seen above.

When the user confirms the selection, the app gets access to a kind of “virtual” photo library that only contains those few photos they’ve selected. To the app it looks almost like a normal photo library, it just has 5 photos in it instead of 2000 – that’s how it can work in existing apps. The app can’t access the remaining photos in any way, or even have any idea how many there are in total.

It can however tell whether it got access to the full library, or some limited subset. You can use the PHPhotoLibrary.authorizationStatus method for this (which has an updated API – it now requires an accessLevel parameter, which is .addOnly or .readWrite):

switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
case .notDetermined:
    // ask for access
case .restricted, .denied:
    // sorry
case .authorized:
    // we have full access

// new option: 
case .limited:
    // we only got access to a part of the library
}

To ask for access, you also need to pass the accessLevel parameter (remember to include a NSPhotoLibraryUsageDescription key in your Info.plist):

PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
    // …
}

For backwards compatibility, the old (deprecated) versions without the parameter return .authorized even when you get limited access.

Updating the selection

You’re probably asking now: how can the user update the selection? If we’re talking about an app like Twitter or Facebook Messenger, the user will only select a few photos that they want to share, but next time when they want to post a photo, they will already be authorized – so the popup won’t appear, and they will just be choosing from the same few photos they chose last time. Not good.

So there are a few ways to solve this:

1) Settings

The user can always go to the Settings app, the Privacy section and update their selection there. However, they need to first know that there is such option and where to find it (the app can’t even deep-link to this specific page), so this is more like a last resort fallback.

2) Repeated alert

By default if the user grants limited access to the photo library to an app, they will see the same popup again after the app is restarted, and through that popup they can update their selection. This is also more of a way to somehow imperfectly support apps that haven’t been updated to the latest SDK – it solves the problem, but in an awkward way and only partially, since the popup won’t appear if you just hide the app, take a few more photos and open it again to share them, and the app doesn’t restart in the meantime.

3) Showing the selection UI again

If you want handle this properly, the recommended way is to manually request to show the selection UI again. Apple explains that if you have an app that requires full access to the photo library (e.g. some app whose main purpose is to let you browse and organize the photo library), you should add some kind of button in your UI that triggers the selection screen again. This button should only appear if authorizationStatus is .limited, and hide if the user grants the app full access.

To show the selection UI, call this new method:

PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)

The selection screen hides automatically when the user completes selection. You will not be notified of the result through any delegate method – you need to use a “change observer” to track when the set of available photos changes. Implement the protocol PHPhotoLibraryChangeObserver and call the register(_ observer:) method on the PHPhotoLibrary:

PHPhotoLibrary.shared().register(self)

func photoLibraryDidChange(_ changeInstance: PHChange) {
    // ...
}

Once you do that, it makes sense to disable that automatic alert mentioned in point 2 above – to do that, add the key PHPhotoLibraryPreventAutomaticLimitedAccessAlert to your Info.plist.

4) Using a system picker

The best option though is that you don’t ask for access to the photo library at all 😏 Remember the carrot? If you use the new system picker, you don’t need to ask for photo library authorization. The picker runs in a separate process, it handles the selection for you and sends you back only what the user selected, so they implicitly grant you access to those photos they picked. No other popups, no checking for authorization.

So if you have an app that currently uses some kind of sliding sheet showing recent photos from which the user picks one to attach it to a post, you really, really should consider just using the system picker, instead of keeping the sheet as a kind of “staging area” and adding another unnecessary step to the flow.

Saving to the library

One special case is when you only need to save to the library, but don’t need to read from it – e.g. you want to let your users save some pictures from a feed or a website. In this case, you only need to ask for an “add only” access, which users may be more likely to grant if it’s obvious that your app doesn’t have any legitimate need for a read access. This is mostly unchanged from earlier iOS versions.

To save a picture to user’s library, you can use this method:

UIImageWriteToSavedPhotosAlbum(image, self, #selector(onImageSaved), nil)

Or just pass nils if you don’t need a callback:

UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)

If you don’t have any authorization at this point yet, it will trigger a popup asking about one – but it uses a different wording and options that the one for read-write access, making it clear that this is about add-only access:

You also need to include the usage key NSPhotoLibraryAddUsageDescription in your Info.plist.

If you’d prefer to ask the user for write access explicitly, you can use the same requestAuthorization method:

PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
    …
}

It will be interesting to see in the coming months how popular apps like Twitter, Facebook, Messenger etc. react to this new situation. The ideal scenario would be that they all switch to PHPicker and avoid the trouble with limited access – this is also, I believe, better UX for the users than if they insist on using library access and presentLimitedLibraryPicker. The worst case scenario is that they do nothing, assume that most users don’t care about privacy or will be too lazy and will just grant them full access, and those who insist on protecting their private photos will be left with working but kinda awkward user experience. Or maybe they’ll figure out something that works well – we’ll see.

1) Yes, I know that iCloud Drive is not end-to-end encrypted – but I trust Apple infinitely more than I trust Google and Dropbox. Hopefully they will add full encryption at some point – they are slowly expanding the range of things that are end-to-end encrypted, e.g. last year they’ve added some synced Safari data to the list. ↩︎

2) This doesn’t seem to currenly work in the simulator in beta 1, including in Apple’s sample code from the talk about the picker. I haven’t tried on a real device. ↩︎


Leave a comment

*

*
This will only be used to display your Gravatar image.

*

What property can you use on iOS to get a unique device ID that the user can reset in Settings?

*