如何使用PHPicker在iOS系統(tǒng)無授權(quán)下獲取資源
自iOS14系統(tǒng)開始,蘋果加強了用戶隱私和安全功能。新增了“Limited Photo Library Access”模式,同時在授權(quán)彈窗中增加了“Select Photo”選項。這意味著用戶可以在應(yīng)用程序請求訪問相冊時選擇部分照片供應(yīng)用程序讀取。從應(yīng)用程序的角度來看,它只能訪問到用戶選擇的這幾張照片,無法得知其他照片的存在。然而,并非所有普通用戶都能夠正確理解這一機制,實際用戶反饋中也反映出了一些誤解。蘋果推薦使用新的PHPicke來解決這個問題。
在本篇文章中,我將詳細(xì)介紹如何正確使用PHPicker以及何時應(yīng)該使用PHPicker。我撰寫這篇文章的原因是:在嘗試使用PHPicker訪問資源庫時遇到了一些問題?;ヂ?lián)網(wǎng)上的許多文章提供的方法都是錯誤的,從而導(dǎo)致了對PHPicker和iOS權(quán)限的一些核心問題的誤解。
01PHPicker是什么?
從iOS14開始,PHPicker是系統(tǒng)提供的Picker ,它允許你從用戶的照片庫中訪問照片和視頻。新的PHPicker類不是在UIKit框架中的,而是位于PhotosUI框架中,包括:
- PHPickerViewController
- PHPickerConfiguration
- PHPickerFilter
- PHPickerResult
當(dāng)你展現(xiàn)一個PHPickerViewController,它有一個PHPickerConfiguration配置來告訴它要選擇多少個媒體項,以及需要選擇的媒體類型。通過 PHPickerConfiguration的filter屬性,配置可選擇的媒體類型,它的選項可以是任意組合:圖片、實況照片或視頻。通過PHPickerConfiguration的selectionLimit屬性來配置用戶可以選擇的媒體項數(shù)量。
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
圖片
02真的需要用戶授權(quán)嗎?
當(dāng)用戶給予受限訪問模式時,如果需要獲得未授權(quán)的額外資源,網(wǎng)絡(luò)上很多文章建議你使用PHAsset和PHPicker來獲取額外的數(shù)據(jù),這樣做的問題是,你必須具有訪問資源庫的權(quán)限,這違背了蘋果建議的使用PHPicker的初衷:在不請求權(quán)限的情況下使用的選擇器。
我們來模擬一下流程:你的的應(yīng)用程序請求訪問用戶資源庫的權(quán)限,用戶說:“我將只給這個應(yīng)用程序有限的訪問一些照片?!?此時,如果你的應(yīng)用程序打開PHPicker并顯示所有的照片;用戶說:“奇怪,我以為我只給有限的訪問權(quán)限,為什么所有照片都有?”;接下來,用戶選擇了一張他沒有給我們訪問權(quán)限的照片。應(yīng)用程序現(xiàn)在需要什么都不做,為了使用PHAsset獲得他選擇的照片的元數(shù)據(jù)(metadata),他們必須再次更新他們的權(quán)限。用戶感到困惑。
所以,如果你的目的非常明確,就是需要用戶給予額外的資源授權(quán)來獲得 PHAsset 對象,應(yīng)該使用iOS14的新API PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: vc),反之,使用 PHPicker不應(yīng)該申請獲得用戶授權(quán),正確的做法是使用 PHPickerViewControllerDelegate返回的NSItemProvider獲得元數(shù)據(jù)(metadata)信息,我將在稍后詳細(xì)介紹。
03使用PHPicker的方式
1、錯誤方式
下面這段代碼是網(wǎng)絡(luò)上廣泛被轉(zhuǎn)載的一段錯誤代碼:
import UIKit
import PhotosUI
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
}
}
在這段代碼中,你使用PHPickerViewController選擇了受限制的資源,但是在調(diào)用PHAsset.fetchAssets時返回了一個空結(jié)果。這是因為fetchAssets方法只能檢索用戶授權(quán)訪問的所有資源,而在受限制模式下,只有最近的受限制資源可供訪問,所以這種方式是錯誤的!
2、正確方式
PHAsset不應(yīng)該與PHPicker一起使用,這不是使用PHPickeri的正確方法!應(yīng)該使用NSItemProvider。NSItemProvider是一個項目提供程序,用于在拖放或復(fù)制/粘貼活動期間在進(jìn)程之間傳輸數(shù)據(jù)或文件,或者從主機應(yīng)用程序到應(yīng)用程序擴展。使用itemProvider,可以讀取對象的類型,并根據(jù)它是照片、視頻還是其他內(nèi)容來處理它。比較合適的方式如下所示:
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)) {
// 圖片處理
} else if(itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier)) {
// 視頻處理
} else {
// 其他,暫時忽略
}
}
}
04獲得圖片與圖片元數(shù)據(jù)
通過前一步驟,我們已經(jīng)知道了資源的類型。接下來通過NSItemProvider的API加載圖片內(nèi)容和獲得元數(shù)據(jù)信息;查閱 NSItemProvider(1)文檔,可以看到加載數(shù)據(jù),主要提供了下面幾種API:
- loadDataRepresentation:返回資源Data數(shù)據(jù)
- loadFileRepresentation:返回資源URL
- loadObject:指定資源類型返回
這里我推薦使用 loadDataRepresentation,返回Data數(shù)據(jù),方便下一步獲得元數(shù)據(jù)信息。
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)) {
// 圖片處理
itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
//處理業(yè)務(wù)model轉(zhuǎn)換
if let model = self.createPhotoResourcesModel(data: data, assetIdentifier: assetIdentifier){
self.albumViewModel.selectArrayAddObject(model)
DispatchQueue.main.async {
//更新UI
}
}
}
} else if(itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier)) {
// 視頻處理
} else {
// 其他,暫時忽略
}
}
}
在處理業(yè)務(wù)model轉(zhuǎn)換函數(shù)中,由于Data類型很容易轉(zhuǎn)換成UIImage,并且通過將Data轉(zhuǎn)換為CFData 類型,可以通過系統(tǒng)預(yù)設(shè)的key/value鍵值對獲得元數(shù)據(jù)信息,
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;
這里我們使用了Github上開源的 ExifData(2) 代碼,完整實現(xiàn)了所有字段的獲取封裝,使用起來非常方便。
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處理特殊格式圖片
如果用戶在資源庫中選擇了一張WebP格式圖片或者GIF動圖,由于展示所需代碼和形式均不同,所以需要特別區(qū)分,那么如何來區(qū)別處理呢?
我們可以通過UTType來具體區(qū)分不同類型,UTType是Uniform Type Identifier的縮寫,用于標(biāo)識特定類型的文件或數(shù)據(jù)。在macOS和iOS等操作系統(tǒng)中,UTType通常用于識別文件類型、將文件分組到合適的應(yīng)用程序中、在不同應(yīng)用程序之間共享數(shù)據(jù)等。
UTType由兩部分組成:類型標(biāo)識符(type identifier)和類型標(biāo)簽(type tag)。類型標(biāo)識符是一串唯一的字符串,用于標(biāo)識特定類型的文件或數(shù)據(jù),通常采用反向DNS風(fēng)格的命名方式,如com.adobe.pdf、public.image等。類型標(biāo)簽是一個可選的字符串,用于描述特定類型的文件或數(shù)據(jù),例如"PDF document"或"JPEG image"等。
if(itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)) {
// 圖片處理
// 判斷webp
if itemProvider.hasItemConformingToTypeIdentifier(UTType.webP.identifier){
//處理webp
}
//判斷GIF
if itemProvider.hasItemConformingToTypeIdentifier(UTType.gif.identifier){
//處理GIF
}
}
06獲得視頻
獲得視頻時,推薦使用loadFileRepresentation,返回URL,通過URL可以獲得 AVAsset。
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)) {
// 圖片處理
} else if(itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier)) {
// 視頻處理
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
//業(yè)務(wù)model轉(zhuǎn)換
if let model = self.createVideoResourcesModel(url: url, assetIdentifier: assetIdentifier){
self.albumViewModel.selectArrayAddObject(model)
DispatchQueue.main.async {
//展示UI
}
}
}
} else {
// 其他,暫時忽略
}
}
}
07獲取加載進(jìn)度
當(dāng)獲取的資源文件較大時,我們需要獲得加載數(shù)據(jù)的進(jìn)度,此時可以使用 NSItemProvider的加載數(shù)據(jù)函數(shù)提供的返回值NSProgress對象。
var progress:Progress?
progress = itemProvider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in
//...
}
//添加觀察者
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("fractinotallow=\(self.progress?.fractionCompleted)")
}
}
在上面的示例代碼中,我們首先創(chuàng)建了一個NSItemProvider對象和一個指定類型標(biāo)識符。然后,我們使用 loadDataRepresentation(forTypeIdentifier:completionHandler:) 方法加載數(shù)據(jù),并返回一個NSProgress對象。我們可以將該對象添加為觀察者,并在觀察者的回調(diào)方法中更新進(jìn)度條的值。
請注意,NSProgress對象是線程安全的,因此可以在不同的線程中使用。此外,如果你需要在多個地方使用同一個NSProgress對象;
NSProgress對象的fractionCompleted屬性,該屬性表示任務(wù)的完成度,其值在0.0和1.0之間。
06總結(jié)
本文要點包含以下:
- PHPicker是iOS14開始引入的新組件,它允許在不需要用戶授權(quán)的情況下訪問照片庫的所有資源;
- 使用PHPicker的正確方式是通過PHPickerViewControllerDelegate回調(diào)返回的NSItemProvider來獲取所選資源,而不是通過PHAsset來獲取,后者需要提前獲取用戶的相冊訪問授權(quán);
- 通過NSItemProvider可以判斷資源類型,加載資源數(shù)據(jù)或文件URL,獲取圖片、視頻等多媒體資源;
- 對于圖片,可以通過loadDataRepresentation獲取Data,并利用該Data獲取圖片元數(shù)據(jù)信息。對于視頻,可以通過loadFileRepresentation獲取URL,并利用URL獲取AVAsset;
- 通過UTType可以進(jìn)一步判斷特殊格式資源如webp、gif等進(jìn)行不同處理;
- 可以通過NSProgress監(jiān)聽資源加載進(jìn)度;
- 正確使用PHPicker可以避免引起用戶疑惑,提高用戶體驗,是iOS14訪問多媒體資源的推薦方式。
總之,本文詳細(xì)介紹了在iOS14中如何正確使用PHPicker訪問用戶選擇的部分照片資源,其要點是不需要提前獲取授權(quán),通過NSItemProvider處理多媒體資源,這是一種更符合系統(tǒng)設(shè)計初衷和提高用戶體驗的方式。
標(biāo)注參考鏈接:
(1)https://developer.apple.com/documentation/foundation/nsitemprovider
(2)https://gist.github.com/lukebrandonfarrell/961a6dbc8367f0ac9cabc89b0052d1fe