How to use PHPicker to obtain resources without authorization on iOS system

e2f1a44d633722a14c1bced7ca102c5a.jpeg

0bf717fec16f39d99ac0000b309c4c32.gif

Number of words in this article: 2766

Estimated reading time: 18 minutes

47efbec559d1c789d730695d8c592f00.png

Since the iOS14 system, Apple has strengthened user privacy and security features. A new “Limited Photo Library Access” mode has been added, and a “Select Photo” option has been added to the authorization pop-up window. This means that the user can select a subset of photos for the application to read when the application requests access to the album. From the application’s perspective, it can only access the photos selected by the user and cannot know the existence of other photos. However, not all ordinary users can correctly understand this mechanism, and some misunderstandings are also reflected in actual user feedback. Apple recommends using the new PHPicke to solve this problem.

In this article, I will explain in detail how to use PHPicker correctly and when you should use PHPicker. The reason I’m writing this post is because I’m having some issues trying to access the repository using PHPicker. Many articles on the Internet provide incorrect methods, leading to misunderstandings about some core issues with PHPicker and iOS permissions.

01

What is PHPicker?

Starting with iOS 14, PHPicker is a system-provided Picker that allows you to access photos and videos from the user’s photo library. The new PHPicker class is not in the UIKit framework, but in the PhotosUI framework, including:

  • PHPickerViewController

  • PHPickerConfiguration

  • PHPickerFilter

  • PHPickerResult

When you present a PHPickerViewController, it has a PHPickerConfiguration configuration that tells it how many media items to select, and the type of media to select. Configure the selectable media types through the filter attribute of PHPickerConfiguration. Its options can be any combination: pictures, live photos, or videos. Configure the number of media items that the user can select through the selectionLimit attribute of PHPickerConfiguration.

let photoLibrary = PHPhotoLibrary.shared()
var config = PHPickerConfiguration(photoLibrary: photoLibrary)
                        
let selectedCount = self.albumViewModel.selectArray.count
let limited = min(4-selectedCount, 4)
config.selectionLimit = (type == .pic ? limited : 1)
config.filter = (type == .pic ? .images : .videos)
let pickerViewController = PHPickerViewController(configuration: config)
pickerViewController.delegate = self
self.viewController?.present(pickerViewController, ani

ce6def66683a838cda5b12acda7b8898.png

02

Does user authorization really need to be used?

When the user is in restricted access mode, if you need to obtain unauthorized additional resources, many articles on the Internet recommend that you use PHAsset and PHPicker to obtain additional data. The problem with this is that you must have permission to access the resource library, which It goes against the original purpose of using PHPicker as recommended by Apple: to use a selector without asking for permission.

Let’s simulate the process: your application requests permission to access the user’s library, and the user says: “I will only give this application limited access to some photos.” At this point, if your application opens PHPicker and displays all photo; the user said: “Weird, I thought I only gave limited access, why are all the photos there?”; Next, the user selected a photo that he did not give us access to. The app needs to do nothing now, in order to use PHAsset to get the metadata of the photo he selected, they have to update their permissions again. Users are confused.

Therefore, if your purpose is very clear, that is, you need the user to grant additional resource authorization to obtain the PHAsset object, you should use the new API PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: vc) in iOS14. On the contrary, you should not apply to obtain the user when using PHPicker. Authorization, the correct approach is to use the NSItemProvider returned by PHPickerViewControllerDelegate to obtain metadata (metadata) information, which I will introduce in detail later.

03

How to use PHPicker

1. Wrong way

The following code is an error code that is widely reproduced on the Internet:

import UIKit
importPhotosUI
class PhotoKitPickerViewController: UIViewController, PHPickerViewControllerDelegate {
    @IBAction func presentPicker(_ sender: Any) {
        let photoLibrary = PHPhotoLibrary.shared()
  let configuration = PHPickerConfiguration(photoLibrary: photoLibrary)
  let picker = PHPickerViewController(configuration: configuration)
  picker.delegate = self
  present(picker, animated: true)
 }
  
 func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
  picker.dismiss(animated: true)
  let identifiers = results.compactMap(\.assetIdentifier)
  let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)
    
  // TODO: Do something with the fetch result if you have Photos Library access
 }
}

In this code, you select a restricted resource using PHPickerViewController, but an empty result is returned when calling PHAsset.fetchAssets. This is because the fetchAssets method can only retrieve all resources that the user is authorized to access, and in restricted mode, only the most recent restricted resources are accessible, so this approach is wrong!

2. Correct way

PHAsset should not be used with PHPicker, this is not the correct way to use PHPickeri! NSItemProvider should be used. NSItemProvider is an item provider used to transfer data or files between processes during drag-and-drop or copy/paste activities, or from a host application to an application extension. Using the itemProvider, you can read the type of the object and handle it depending on whether it is a photo, video, or other content. A more appropriate approach is as follows:

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    picker.dismiss(animated: true)
    for result in results {
        let itemProvider: NSItemProvider = result.itemProvider;
        if(itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)) {
            //Image processing
        } else if(itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier)) {
            // video processing
        } else {
            //Others, ignore for now
        }
    }
}

04

Get images and image metadata

Through the previous step, we already know the type of resource. Next, load the image content and obtain metadata information through the API of NSItemProvider; check the NSItemProvider(1) document to see the loading data, which mainly provides the following APIs:

  • loadDataRepresentation: Return resource Data data

  • loadFileRepresentation: Returns the resource URL

  • loadObject: returns the specified resource type

Here I recommend using loadDataRepresentation to return Data data to facilitate the next step to obtain metadata information.

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    picker.dismiss(animated: true)
    for result in results {
        let itemProvider: NSItemProvider = result.itemProvider;
        if(itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)) {
            //Image processing
            itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
                    //Process business model conversion
                    if let model = self.createPhotoResourcesModel(data: data, assetIdentifier: assetIdentifier){
                        self.albumViewModel.selectArrayAddObject(model)
                        
                        DispatchQueue.main.async {
                            //update UI
                        }
                    }
              }
            
        } else if(itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier)) {
            // video processing
        } else {
            //Others, ignore for now
        }
    }
}

In the business model conversion function, since the Data type is easily converted into UIImage, and by converting Data into the CFData type, metadata information can be obtained through the system’s preset key/value pairs.

let imgSrc = CGImageSourceCreateWithData(data, options as CFDictionary)
let metadata = CGImageSourceCopyPropertiesAtIndex(imgSrc, 0, options as CFDictionary)
colorModel = metadata[kCGImagePropertyColorModel] as? String
pixelWidth = metadata[kCGImagePropertyPixelWidth] as? Double;
pixelHeight = metadata[kCGImagePropertyPixelHeight] as? Double;

Here we use the open source ExifData(2) code on Github, which completely implements the acquisition and encapsulation of all fields, and is very convenient to use.

func createPhotoResourcesModel(data:Data?,
                            assetIdentifier:String?) -> SNSResourcesModel? {
        guard let imageData = data, let uiimage = UIImage(data: imageData) else {
            return nil
        }
        let model = SNSResourcesModel()
        model.assetLocalIdentifier = assetIdentifier
        model.assetType = .photo
        model.assetSource = .album
        model.originImage = uiimage
        model.bigPreviewImage = uiimage
        
        let exifData = ExifData(data: imageData)
        model.pixelWidth = Int(exifData.pixelWidth  0)
        model.pixelHeight = Int(exifData.pixelHeight  0)
        if imageData.imageType == .GIF{
            model.isGif = true
            model.gifData = imageData
        }
        return model
}

05

Processing special format images

If the user selects a WebP format picture or GIF animation in the resource library, since the code and format required for display are different, it needs to be specially distinguished. So how to deal with the difference?

We can specifically distinguish different types through UTType. UTType is the abbreviation of Uniform Type Identifier, which is used to identify specific types of files or data. In operating systems such as macOS and iOS, UTType is commonly used to identify file types, group files into appropriate applications, share data between different applications, etc.

UTType consists of two parts: type identifier (type identifier) and type tag (type tag). A type identifier is a unique string used to identify a specific type of file or data, usually using reverse DNS style naming, such as com.adobe.pdf, public.image, etc. The type tag is an optional string describing a specific type of file or data, such as “PDF document” or “JPEG image”.

if(itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)) {
    //Image processing
    // Determine webp
    if itemProvider.hasItemConformingToTypeIdentifier(UTType.webP.identifier){
        //process webp
    }
    //Judge GIF
    if itemProvider.hasItemConformingToTypeIdentifier(UTType.gif.identifier){
        //process GIF
    }
}

06

Get Video

When obtaining a video, it is recommended to use loadFileRepresentation to return the URL, through which the AVAsset can be obtained.

func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
    picker.dismiss(animated: true)
    for result in results {
        let itemProvider: NSItemProvider = result.itemProvider;
        if(itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)) {
            //Image processing
        } else if(itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier)) {
            // video processing
            itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
                    //Business model conversion
                    if let model = self.createVideoResourcesModel(url: url, assetIdentifier: assetIdentifier){
                        self.albumViewModel.selectArrayAddObject(model)
                        
                        DispatchQueue.main.async {
                            //Show UI
                        }
                    }
            }
        } else {
            //Others, ignore for now
        }
    }
}

06

Get loading progress

When the obtained resource file is large, we need to obtain the progress of loading data. At this time, we can use the return value NSProgress object provided by the NSItemProvider’s loading data function.

var progress:Progress?
progress = itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
    //...
}
//Add observer
progress?.addObserver(self, forKeyPath: "fractionCompleted", options: [.new], context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if keyPath == "fractionCompleted" {
        print("fractionCompleted=\(self.progress?.fractionCompleted)")
    }
}

In the sample code above, we first create an NSItemProvider object and a specified type identifier. Then, we use the loadDataRepresentation(forTypeIdentifier:completionHandler:) method to load the data and return an NSProgress object. We can add this object as an observer and update the value of the progress bar in the observer’s callback method.

Note that NSProgress objects are thread-safe and can therefore be used in different threads. Additionally, if you need to use the sameNSProgressobject in multiple places;

The fractionCompleted attribute of the NSProgress object represents the completion of the task and its value is between 0.0 and 1.0.

06

Summary

The main points of this article include the following:

  • PHPicker is a new component introduced in iOS14, which allows access to all resources of the photo library without user authorization;

  • The correct way to use PHPicker is to obtain the selected resource through the NSItemProvider returned by the PHPickerViewControllerDelegate callback, rather than through PHAsset, which requires obtaining the user’s album access authorization in advance;

  • NSItemProvider can be used to determine resource types, load resource data or file URLs, and obtain multimedia resources such as pictures and videos;

  • For pictures, you can obtain Data through loadDataRepresentation and use this Data to obtain picture metadata information. For videos, you can get the URL through loadFileRepresentation and use the URL to get AVAsset;

  • Through UTType, special format resources such as webp, gif, etc. can be further determined for different processing;

  • Resource loading progress can be monitored through NSProgress;

  • Proper use of PHPicker can avoid user confusion and improve user experience. It is the recommended way to access multimedia resources in iOS14.

In short, this article introduces in detail how to correctly use PHPicker to access some photo resources selected by the user in iOS14. The main point is that there is no need to obtain authorization in advance and multimedia resources are processed through NSItemProvider. This is a method that is more in line with the original intention of the system design and improves the user experience. Way.

Mark reference link:

(1)https://developer.apple.com/documentation/foundation/nsitemprovider

(2)https://gist.github.com/lukebrandonfarrell/961a6dbc8367f0ac9cabc89b0052d1fe

Other reference links:

(1)https://itnext.io/the-right-way-to-use-phpicker-and-retrieve-exif-data-without-requesting-library-permissions-in-336c13f87e3f

(2)https://swiftsenpai.com/development/webp-phpickerviewcontroller/

(3)https://gist.github.com/lukebrandonfarrell/961a6dbc8367f0ac9cabc89b0052d1fe

(4)https://www.jianshu.com/p/5e7aacfa4374

(5)https://developer.apple.com/forums/thread/650902