MacKuba

🍎 Kuba Suder's blog on Mac & iOS development

WWDC 21

Improve access to Photos in your app

Categories: Photos, UIKit 0 comments Watch the video

Improvements to the Photo Picker

Privacy:

When an app doesn't ask for a Photo Library access and only presents the Photo Picker β€“ which runs out of process and automatically has access to the whole library, from which the user can pick some photos for the app β€“ it was possible for people to misinterpret this workflow and assume that the app itself had a full access to the whole photo library, even if it didn't

So now, in the Settings app, on the Photos Privacy screen, there is a new section for apps that only use the Photos Picker, titled "Apps with one-time photo selection"

Ordered selection:

In some apps, users may need to be able to select a few photos in a specific order (e.g. when adding them to a social media post)

In iOS 15, your app can configure the picker to show selection order as badges β‘  β‘‘ β‘’ etc.

To do that, set the selection property in the picker configuration to .ordered:

var configuration = PHPickerConfiguration()
configuration.selectionLimit = 0
configuration.selection = .ordered

Selection adjustment:

You can now preselect some photos in the picker to display user's previous selection that they can edit, adding additional photos or removing some previously selected ones

To preselect photos, pass an array of asset identifiers to the picker configuration:

let assetIdentifiers: [String] = previousSelection

var configuration = PHPickerConfiguration(photoLibrary: .shared)
configuration.selectionLimit = 0
configuration.preselectedAssetIdentifiers = assetIdentifiers

⚠️ One caveat: when the picker returns updated results, all photos that have been previously selected (and included in the preselection config you provide) will not include the item providers

So you need to keep the original list of photos with their data until after the updated picker results are returned

If the preselected picker dialog is cancelled, it will return the preselected assets you've provided with all item providers empty

In the delegate callback, you need to merge together the old results with the new results, taking the value from the old results for any photo that was selected previously (since there won't be an item provider in the new one):

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

    let existingSelection: [String: PHPickerResult] = self.lastSelection
    var newSelection = [String: PHPickerResult]()

    for result in results {
        let identifier = result.assetIdentifier!
        newSelection[identifier] = existingSelection[identifier] ?? result
    }

    self.lastSelection = newSelection

    // do something with selected assets
}

Reporting progress:

Some assets that the user selects in the picker may not be immediately available and need to be downloaded from the cloud first, which may take a moment (especially for videos) β€“ this can happen if they're selecting something from their iCloud Photos and the "Optimize Storage" option is turned on

In that case, previously your app could only show a spinner indicator, since it didn't have access to information about the download progress

Now, the asset's item provider can give you a Progress object that you can use to track download progress and show a more appropriate loading UI:

let result: PHPickerResult = ...
let provider: NSItemProvider = result.itemProvider
let identifier: String = UTType.movie.identifier

if provider.hasItemConformingToTypeIdentifier(identifier) {
    let progress: Progress = provider.loadFileRepresentation(
        forTypeIdentifier: identifier
    ) { url, error in
        // Do something with the video, or handle the error
    }

    // Show progress in the meantime
}

New cloud identifier APIs

There are some categories of apps that require full or partial direct access to the user's photo library, e.g. photo editing apps, camera apps or apps whose purpose is to display the photo library in some specific way

Those apps use the PhotoKit APIs to access and modify the photo library

When using those APIs, assets are returned with unique identifiers that your app can save for later and then use again to retrieve the same assets on the next launch

These identifiers are specific to each device, even if some of the assets are synced between devices using iCloud Photos

If your app syncs some user-generated data that references user's photos between devices, and you want to access the same photos on multiple devices, you can use the new cloud identifiers that identify the same photo globally

There is a mapping between local identifiers and cloud identifiers and you can use a cloud identifier to look up a local copy of the photo

Cloud identifiers work even if the device is not signed into iCloud Photos (or even never was)

The new identifiers are represented by PHCloudIdentifier objects

How to use cloud identifiers:

1) Get some local photos from a device's library using CloudKit

2) Map local identifiers to cloud identifiers:

let cloudMappings = PHPhotoLibrary.shared()
        .cloudIdentifierMappings(forLocalIdentifiers: localIdentifiers)

for (localIdentifier, cloudMapping) in cloudMappings {
    if let cloudIdentifier = cloudMapping.cloudIdentifier {
        // save the cloudIdentifier for later
        resolved[localIdentifier] = cloudIdentifier
    } else {
        // handle the cloudMapping error
    }
}

3) Transfer the cloud identifiers to other devices using whatever communication method you want to use (iCloud / CloudKit etc.)

β†’ use stringValue to encode the identifier to a string

4) Look up local identifiers on each device based on the cloud identifiers in the same way:

let localMappings = PHPhotoLibrary.shared()
        .localIdentifierMappings(for: cloudIdentifiers)

for (cloudIdentifier, localMapping) in localMappings {
    if let localIdentifier = localMapping.localIdentifier {
        // add the localIdentifier to our resolved assets
        resolved[cloudIdentifier] = localIdentifier
    } else {
        // handle the localMapping error
    }
}

5) Use the local identifiers to fetch the assets and display them

Error handling:

The mapping in both directions may return an error instead of the identifier, so you need to be able to handle that case

There are two possible kinds of errors to take into account:

1) Identifier Not Found β€“ if the app isn't able to find or access the relevant record:

let error = localMapping.error! as NSError

if error.code == PHPhotosError.identifierNotFound.rawValue {
    // couldn't find this photo, add it to the missing photos list
    missingPhotos.append(cloudIdentifier)
}

2) Multiple Identifiers Found β€“ this can happen if the cloud state isn't completely in sync, and the app tries to find the image using content match and finds multiple copies; in this case, the error info will contain the list of matching identifiers under PHLocalIdentifiersErrorKey

let error = localMapping.error! as NSError

if error.code == PHPhotosError.multipleIdentifiersFound.rawValue {
    // found multiple matches, prompt the user to pick one
    let matches = error.userInfo[PHLocalIdentifiersErrorKey] as! [String]
    multipleMatches[cloudIdentifier] = matches
}

Note: looking up cloud identifiers takes some work, so use local identifiers for normal app interactions and map them to cloud identifiers only for syncing with other devices

Updates to the limited library

The limited library is when you access the user's photo library using PhotoKit after the user has requested to only share selected photos with your app

This is designed to work transparently for your app β€“ all PhotoKit APIs work fine, but they work as if the photo library only contained those selected photos and nothing else

See last year's talk Handle the Limited Photos Library in Your App for more info

In iOS 15, apps can now create, fetch and update their own photo albums within the user's photo library when running in the limited library mode

Also, when you call photoLibrary.presentLimitedLibraryPicker() to let the user adjust a previous selection of photos shared with your app, you can now provide a callback which will be given a list of photos that have just been added to the selection:

let library = PHPhotoLibrary.shared()
library.presentLimitedLibraryPicker(from: controller) { addedIdentifiers in
   // fetch the newly added photos and use them in your app 
}

If your app still uses the old Assets Library framework that was deprecated in iOS 9 (ALAssets*) β€“ please switch to PhotoKit and the Photos Picker, the old API will be removed in a future SDK



Leave a comment

*

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

*

What's the name of the base class of all AppKit and UIKit classes?

*