如何使用PHPicker在iOS系统无授权下获取资源

news/2025/2/12 23:36:08/

e2f1a44d633722a14c1bced7ca102c5a.jpeg

0bf717fec16f39d99ac0000b309c4c32.gif

本文字数:2766

预计阅读时间:18分钟

47efbec559d1c789d730695d8c592f00.png

自iOS14系统开始,苹果加强了用户隐私和安全功能。新增了“Limited Photo Library Access”模式,同时在授权弹窗中增加了“Select Photo”选项。这意味着用户可以在应用程序请求访问相册时选择部分照片供应用程序读取。从应用程序的角度来看,它只能访问到用户选择的这几张照片,无法得知其他照片的存在。然而,并非所有普通用户都能够正确理解这一机制,实际用户反馈中也反映出了一些误解。苹果推荐使用新的PHPicke来解决这个问题。

在本篇文章中,我将详细介绍如何正确使用PHPicker以及何时应该使用PHPicker。我撰写这篇文章的原因是:在尝试使用PHPicker访问资源库时遇到了一些问题。互联网上的许多文章提供的方法都是错误的,从而导致了对PHPicker和iOS权限的一些核心问题的误解。

01

PHPicker是什么?

从iOS14开始,PHPicker是系统提供的Picker ,它允许你从用户的照片库中访问照片和视频。新的PHPicker类不是在UIKit框架中的,而是位于PhotosUI框架中,包括:

  • PHPickerViewController

  • PHPickerConfiguration

  • PHPickerFilter

  • PHPickerResult

当你展现一个PHPickerViewController,它有一个PHPickerConfiguration配置来告诉它要选择多少个媒体项,以及需要选择的媒体类型。通过 PHPickerConfiguration的filter属性,配置可选择的媒体类型,它的选项可以是任意组合:图片、实况照片或视频。通过PHPickerConfiguration的selectionLimit属性来配置用户可以选择的媒体项数量。

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

真的需要用户授权吗?

当用户给予受限访问模式时,如果需要获得未授权的额外资源,网络上很多文章建议你使用PHAsset和PHPicker来获取额外的数据,这样做的问题是,你必须具有访问资源库的权限,这违背了苹果建议的使用PHPicker的初衷:在不请求权限的情况下使用的选择器。

我们来模拟一下流程:你的的应用程序请求访问用户资源库的权限,用户说:“我将只给这个应用程序有限的访问一些照片。” 此时,如果你的应用程序打开PHPicker并显示所有的照片;用户说:“奇怪,我以为我只给有限的访问权限,为什么所有照片都有?”;接下来,用户选择了一张他没有给我们访问权限的照片。应用程序现在需要什么都不做,为了使用PHAsset获得他选择的照片的元数据(metadata),他们必须再次更新他们的权限。用户感到困惑。

所以,如果你的目的非常明确,就是需要用户给予额外的资源授权来获得 PHAsset 对象,应该使用iOS14的新API PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: vc),反之,使用 PHPicker不应该申请获得用户授权,正确的做法是使用 PHPickerViewControllerDelegate返回的NSItemProvider获得元数据(metadata)信息,我将在稍后详细介绍。

03

使用PHPicker的方式

1、错误方式

下面这段代码是网络上广泛被转载的一段错误代码:

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 = selfpresent(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选择了受限制的资源,但是在调用PHAsset.fetchAssets时返回了一个空结果。这是因为fetchAssets方法只能检索用户授权访问的所有资源,而在受限制模式下,只有最近的受限制资源可供访问,所以这种方式是错误的!

2、正确方式

PHAsset不应该与PHPicker一起使用,这不是使用PHPickeri的正确方法!应该使用NSItemProvider。NSItemProvider是一个项目提供程序,用于在拖放或复制/粘贴活动期间在进程之间传输数据或文件,或者从主机应用程序到应用程序扩展。使用itemProvider,可以读取对象的类型,并根据它是照片、视频还是其他内容来处理它。比较合适的方式如下所示:

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

获得图片与图片元数据

通过前一步骤,我们已经知道了资源的类型。接下来通过NSItemProvider的API加载图片内容和获得元数据信息;查阅 NSItemProvider(1)文档,可以看到加载数据,主要提供了下面几种API:

  • loadDataRepresentation:返回资源Data数据

  • loadFileRepresentation:返回资源URL

  • loadObject:指定资源类型返回

这里我推荐使用 loadDataRepresentation,返回Data数据,方便下一步获得元数据信息。

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//处理业务model转换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 {// 其他,暂时忽略}}
}

在处理业务model转换函数中,由于Data类型很容易转换成UIImage,并且通过将Data转换为CFData 类型,可以通过系统预设的key/value键值对获得元数据信息,

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) 代码,完整实现了所有字段的获取封装,使用起来非常方便。

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

05

处理特殊格式图片

如果用户在资源库中选择了一张WebP格式图片或者GIF动图,由于展示所需代码和形式均不同,所以需要特别区分,那么如何来区别处理呢?

我们可以通过UTType来具体区分不同类型,UTType是Uniform Type Identifier的缩写,用于标识特定类型的文件或数据。在macOS和iOS等操作系统中,UTType通常用于识别文件类型、将文件分组到合适的应用程序中、在不同应用程序之间共享数据等。

UTType由两部分组成:类型标识符(type identifier)和类型标签(type tag)。类型标识符是一串唯一的字符串,用于标识特定类型的文件或数据,通常采用反向DNS风格的命名方式,如com.adobe.pdf、public.image等。类型标签是一个可选的字符串,用于描述特定类型的文件或数据,例如"PDF document"或"JPEG image"等。

if(itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier)) {// 图片处理// 判断webpif itemProvider.hasItemConformingToTypeIdentifier(UTType.webP.identifier){//处理webp}//判断GIFif 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//业务model转换if let model = self.createVideoResourcesModel(url: url, assetIdentifier: assetIdentifier){self.albumViewModel.selectArrayAddObject(model)DispatchQueue.main.async {//展示UI}}}} else {// 其他,暂时忽略}}
}

06

获取加载进度

当获取的资源文件较大时,我们需要获得加载数据的进度,此时可以使用 NSItemProvider的加载数据函数提供的返回值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("fractionCompleted=\(self.progress?.fractionCompleted)")}
}

在上面的示例代码中,我们首先创建了一个NSItemProvider对象和一个指定类型标识符。然后,我们使用 loadDataRepresentation(forTypeIdentifier:completionHandler:) 方法加载数据,并返回一个NSProgress对象。我们可以将该对象添加为观察者,并在观察者的回调方法中更新进度条的值。

请注意,NSProgress对象是线程安全的,因此可以在不同的线程中使用。此外,如果你需要在多个地方使用同一个NSProgress对象;

NSProgress对象的fractionCompleted属性,该属性表示任务的完成度,其值在0.0和1.0之间。

06

总结

本文要点包含以下:

  • PHPicker是iOS14开始引入的新组件,它允许在不需要用户授权的情况下访问照片库的所有资源;

  • 使用PHPicker的正确方式是通过PHPickerViewControllerDelegate回调返回的NSItemProvider来获取所选资源,而不是通过PHAsset来获取,后者需要提前获取用户的相册访问授权;

  • 通过NSItemProvider可以判断资源类型,加载资源数据或文件URL,获取图片、视频等多媒体资源;

  • 对于图片,可以通过loadDataRepresentation获取Data,并利用该Data获取图片元数据信息。对于视频,可以通过loadFileRepresentation获取URL,并利用URL获取AVAsset;

  • 通过UTType可以进一步判断特殊格式资源如webp、gif等进行不同处理;

  • 可以通过NSProgress监听资源加载进度;

  • 正确使用PHPicker可以避免引起用户疑惑,提高用户体验,是iOS14访问多媒体资源的推荐方式。

总之,本文详细介绍了在iOS14中如何正确使用PHPicker访问用户选择的部分照片资源,其要点是不需要提前获取授权,通过NSItemProvider处理多媒体资源,这是一种更符合系统设计初衷和提高用户体验的方式。

标注参考链接:

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

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

其他参考链接:

(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


http://www.ppmy.cn/news/1184612.html

相关文章

LeetCode 面试题 16.07. 最大数值

文章目录 一、题目二、C# 题解 一、题目 编写一个方法,找出两个数字a和b中最大的那一个。不得使用if-else或其他比较运算符。 示例: 输入: a 1, b 2 输出: 2 点击此处跳转题目。 二、C# 题解 找出最大的数,本质还是…

回归算法|长短期记忆网络LSTM及其优化实现

本期文章将介绍LSTM的原理及其优化实现 序列数据有一个特点,即“没有曾经的过去则不存在当前的现状”,这类数据以时间为纽带,将无数个历史事件串联,构成了当前状态,这种时间构筑起来的事件前后依赖关系称其为时间依赖&…

2015年亚太杯APMCM数学建模大赛B题城市公共交通服务水平动态评价模型求解全过程文档及程序

2015年亚太杯APMCM数学建模大赛 B题 城市公共交通服务水平动态评价模型 原题再现 城市公共交通服务评价是城市公共交通系统建设和提高公共交通运营效率的重要组成部分。对于公交企业,管理和规划部门,传统公交车站、线路和换乘枢纽的规划数据只是基于主…

el-input 给icon图标绑定点击事件

选择suffix-icon&#xff0c;添加点击事件 <temeplate><el-form-item :label"$t(company[Company address])" prop"address"><el-input v-model"enterpriseForm.address"><i slot"suffix" class"el-icon-m…

【每日一题】补档 CF487B. Strip | 数据结构杂烩 -> 单调队列 | 困难

题目内容 原题链接 给定一个长度为 n n n 的数组&#xff0c;将这个数组进行拆分成若干个连续子数组&#xff0c; 使得每个子数组的最大值减去最小值小于等于 s s s &#xff0c; 且每个子数组的长度大于等于 l e n len len 。 问最少可以拆分成多少个连续子数组&#xff0…

modelsim仿真报错:vlog-2388 ‘scl‘ already declared in this scope

问题背景&#xff1a; 1、使用vivado直接仿真的时候没有报错。 2、在vivado中调用modelsim的时候报错。 报错的代码&#xff1a; module iic_write(input clk,input rst,output scl,input en,inout sda);reg scl&#xff1b;……报错的意思是scl已经声明过了&#xff0c;mode…

第 369 场 LeetCode 周赛题解

A 找出数组中的 K-or 值 模拟 class Solution { public:int findKOr(vector<int> &nums, int k) {vector<int> cnt(32);for (auto x: nums)for (int i 0; i < 32; i)if (x >> i & 1)cnt[i];int res 0;for (int i 0; i < 32; i)if (cnt[i] &…

python自动化测试(五):按键模拟输入:全选、复制、清空、粘贴、完成

前置条件&#xff1a; 本地部署&#xff1a;ECShop的版本是3.0.0、Google版本是 Google Chrome65.0.3325.162 (正式版本) &#xff08;32 位&#xff09; Google驱动的selenium版本是3.11.0 目录 一、配置代码 二、键盘组合输入 2.1 全选&#xff1a;ctrl a 2.2 复制…