iOS 14 - 使用 PHPicker 选择照片和视频

现状

绝大多数的 App 都要和相册打交道,选择照片或者视频,要么用来发个朋友圈,要么是放到什么地方做个背景。从 AssertLibrary 到 Photos 框架,苹果已经在多年之前就给相册相关的 API 做过一次大升级了。

通过 Photos 框架,只需要通过下面几步就可以从相册获取到内容了

1、获取相册权限
2、建立 UI 展示相册内容
3、响应用户操作,获取照片或视频的 PHAsset 对象
4、通过 PHImageManager 导出照片或者视频

作为一个合格的开发者,对上面这些步骤基本上都能驾轻就熟了。不过虽然看似简单的几步,其实依然存在着巨大的工作量,特别是建立 UI 并展示相册内容这一条,如果要做到细节完善、体验顺畅,基本上属于一个小型 App 的工作量了。所以很多时候我们会选择使用一些第三方的框架,目前可供选择的优秀作品也很多。看起来感觉岁月静好,可以结束工作去泡咖啡了 :)

变化

然而,事情总是在变化的,一向在折腾之路上狂奔的苹果这次依然没有让我们失望,在相册相关的 API 上带来了两个重大变动:

1、相册权限变动

Photos 框架提供了查询相册授权情况的 api,我们可以通过 PHPhotoLibrary 的 authorizationStatus 方法来查询当前的相册授权情况。查询结果 PHAuthorizationStatus 是个枚举,定义如下:

public enum PHAuthorizationStatus : Int {

    @available(iOS 8, *)
    case notDetermined = 0

    @available(iOS 8, *)
    case restricted = 1

    @available(iOS 8, *)
    case denied = 2

    @available(iOS 8, *)
    case authorized = 3
}

从 iOS 8 引入 Photos 框架一开始,相册权限就存在这几种状态:

  • notDetermined

状态不明确,用户还没有明确授权或拒绝访问,这时候我们一般通过调用 requestAuthorization 方法来显示权限询问弹窗,让用户授权访问。

  • restricted

没有访问权限,这个状态表示设备因为特殊原因被禁止访问相册,可能是基于家长控制设置的权限,也可能是当前设备隶属于某个组织,而组织在权限描述文件中禁止了这台设备的相册权限。

这个状态下,用户也无法主动开启相册权限,所以此时唯一能做的就是告诉用户无法获取相册内容。

  • denied

禁止访问,这个状态表示用户在上一次询问相册权限时明确选择了禁止。这时候我们可以选择提示用户无法访问,或者提示用户主动去设置中开启权限。

  • authorized

允许访问,这个状态表示用户明确同意了相册的授权,此时我们就可以通过 PHAssetCollection 和 PHAsset 相关的 api 来获取相册和其中的照片了。

原本这一切可以很完美的运转,但是如果在 Xcode 12 中查看PHAuthorizationStatus 的定义,会看到从 iOS 14 开始,苹果引入了一个新的权限状态:limited

有限访问权限

苹果在每一年的 WWDC 都特别强调用户隐私,到了今年,苹果终于对相册动手了。现有的方案虽然看起来很完美,但是存在一个重大的隐患:用户往往只需要从相册选一张照片上传,可此时 App 却获取了整个相册所有照片的数据!!

这时候如果开发者没有守住节操底线,那么用户的所有生活照片甚至是私密照片都存在严重的泄漏风险。

新引入的有限访问权限正是为了解决这个问题。在 iOS 14 下,当我们通过 requestAuthorization 向用户获取权限时,弹窗中多了一个选项:“选择部分照片”,此时系统会在 App 进程之外弹出相册让用户选择授权给当前 App 访问的照片。用户完成选择后,App 通过相册相关的 api 就只能获取到指定的这几张照片,这有效的保护了用户的隐私。

不过,这个设计虽然保护了用户的隐私,但是从 App 的角度来看却引入了新的问题:如果我们还是用原先的那一套流程来获取照片,那么在用户选择了部分权限的情况下,每次弹出相册后用户都无法选择其他的照片,解决办法是再次弹出权限询问,让用户选择或者更换指定的照片,然后再回到我们的相册 UI,继续之后的流程。。。

这一套流程明显变得更加复杂并且奇怪了。。。而且作为一个开发者,始终不能忘了用户是不了解技术细节的,在用户的角度来看,他可能会觉得很奇怪:“为什么我每次都只能选这两张照片?这 App 有什么毛病?” 而如果你选择每次都但窗询问权限,那么当用户耐心耗尽的那一天,就是你的 App 被卸载之时。。。

基于从 Beta 版就开始使用 iOS 14 的体验,升级之后所有 App 的第一次访问相册时都会弹窗询问权限,目测 iOS 14 应该是重置了所有 App 的相册授权状态。

2、新的相册组件

好在苹果还是给开发者留了一扇窗的,那就是这篇文章要讲的主题,新成员:PHPicker。

其实上面在用户选择部分照片时,我们就已经接触到这玩意儿了:系统在 App 内部额外弹出的相册,本质上就是一个 PHPicker。

苹果在宣传中说它是基于系统的相册 App 的,具有与系统相册一致的 UI 和操作方式,可以保证用户体验的一致性。并且和相册 App 一样,支持通过人物、地点等关键信息来搜索照片。并且 PHPicker 是在独立进程中运行的,与宿主 App 无关,宿主 App 也无法通过截屏 api 来获取当前屏幕上的照片信息。为了保护用户隐私,苹果真的是在各种细节上严防死守。

既然在用户授权时可以在 App 中弹出这个选择器,那么我们的 App 是否可以主动发起这个弹窗呢?答案是:PHPickerViewController

3、PHPickerViewController

PHPickerViewController 是整个 PHPicker 组件的核心,PHPicker 隶属于 PhothsUI 库,使用之前需要先导入:

import PhotosUI

先来查看这个类的定义:

@available(iOS 14, *)
public class PHPickerViewController : UIViewController {
}

@available(iOS 14, *)
extension PHPickerViewController {

    /// The configuration passed in during initialization.
    public var configuration: PHPickerConfiguration { get }

    /// The delegate to be notified.
    weak public var delegate: PHPickerViewControllerDelegate?

    /// Initializes new picker with the `configuration` the picker should use.
    public convenience init(configuration: PHPickerConfiguration)
}

太简单了!这个类的定义中居然没有任何声明,只是表示了一下自己继承自 UIViewControlelr,以及在扩展中暴露了两个属性和一个构造器。。。

PHPickerConfiguration

查看 PHPickerConfiguration 的定义,发现这里东西稍微多了一点:

  • preferredAssetRepresentationMode: PHPickerConfiguration.AssetRepresentationMode

presentationMode 用来指定导出结果的表示形式,默认值是 automatic, 此时系统会自动执行一些转码之类的操作,并且在注释中明确说了这个模式的实际表现会在之后的发布中修改。。。

果然这种表述就是坑的表现:目前官方论坛上有用户反馈,使用了缺省的 automatic 模式后,从相册导出视频的过程变得巨慢:

对此官方的回复是 automatic 模式可能会在导出时对视频进行转码处理,如果 App 本地能够处理 HEVC 格式的视频,可以指定为 current 模式来跳过转码的过程。

这一条是我在集成过程中遇到的最大的坑之一了,实际测试中,两分钟的原始视频(大约200到300M),使用 automatic 自动转码模式,导出过程耗时达到了难以置信的 5分钟!而改为 current 模式跳过转码之后,导出过程几乎无感(5s以内)。由于我们的 App 本身会将导出的视频压缩转码后再上传,因此果断选择了 current 模式

关于 compatible 模式,注释中用了充满玄学的说法: 尽量选择最合适的形式。由于 PHPicker 相关的资料目前还太少,这个模式的运作方式暂时还摸不清楚,欢迎掉过坑的同学来补充 :)

  • selectionLimit

这个设置很好理解,限制用户最多可以选择的数量

  • filter: PHPickerFilter

filter 提供了有限的筛选模式设置,目前可以指定筛选照片、视频、LivePhoto 几种类型,或者任何他们的组合。这一点上目前还是比较欠缺的,比如说发送朋友圈时需要限制用户只能上传一分钟以内的视频,原来我们具有完全相册访问权限的时候,可以在展示的时候直接过滤掉超过一分钟的视频,或者将他们标志为不可选。目前 PHPicker 并没有提供更详细的筛选配置,所以应对这种需求折中的方法是将视频导出之后获取视频的时长,如果超过限制则提示用户。

弹出选择器

弹出窗口其实没什么好说的,PHPickerViewController 本身还是一个 ViewController,设置好相应的属性和 delegate 后,简单的调用 present 就可以了。

  • delegate: PHPickerViewControllerDelegate

PHPickerViewController 依然是通过 delegate 属性来向宿主 App 返回用户选择的结果。

看定义同样很简单,PHPickerViewControllerDelegate 只定义了一个方法:

public protocol PHPickerViewControllerDelegate : AnyObject {

    /// Called when the user completes a selection or dismisses `PHPickerViewController` using the cancel button.
    ///
    /// The picker won't be automatically dismissed when this method is called.
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult])
}

用户完成选择后,该方法会触发,用户选择的结果会以 PHPickerResult 数组的形式传入,每一个 PHPickerResult 都对应一个照片、视频或者 LivePhoto 的数据。

需要注意的是,注释中明确说明了 PHPickerViewController 不会自动关闭,用户需要在选择完毕后,自行调用 dismiss 方法来关闭选择器。

PHPickerResult 的定义一如既往的的“简洁”,只有 itemProvider 和 assetIdentifier 两个属性,剩下的是继承的 Equatable 和 Hashable 协议的实现。

  • assetIdentifier
    这是选中对象对应的 PHAsset 的 id,可以用这个 id 通过 PHAset 的相关 api 来进行其他操作。不过苹果在 WWDC 的介绍视频中明确强调了如果要访问 PHAsset 的额外信息,依然需要获取到相册权限后才可以执行。
  • itemProvider

NSItemProvider 是一个 iOS 8.0 就存在了的 api,上一次使用还是在 iOS 11 引入 Drag & Drop 的时候了。不过用法依然是一致的:故名思议,这个对象就是用来向我们提供另一个对象的(好像有点绕?) 通过 itemProvider 的 api,我们可以获取到最终的结果,不过这里有点小麻烦:针对照片、视频和 LivePhoto 三种媒体类型,分别要使用不同的 api 来获取

获取照片

获取照片比较简单,通过 NSItemProvider 的 loadObject 方法,并且指定 Class 类型为 UIImage,就可以在回调中得到 UIImage 类型的照片了:

provider.loadObject(ofClass: UIImage.self) { (image, error) in
  // do someting with results
}

获取 LivePhoto

获取 LivePhoto 与获取照片类似,只是需要将 UIImage 替换为 PHLivePhoto。之后你可以通过 PHLivePhotoView 来显示。或者通过 PHAssetResourceManager 获取 LivePhoto 的原始数据。

获取视频

苹果在 WWDC 视频中只演示了如果使用 PHPickerViewController 获取相册照片,但对于如何获取视频只字未提,这就比较尴尬了。目前跟进 PHPicker 的开发者还不多,基本上都搜不到相关资料,一番折腾之后,终于在官方论坛零星找到了几条关于获取视频的讨论。

查看了相关对话之后,总算摸清了获取视频的套路,要稍微复杂一点:框架开发者明确指出需要使用 loadFileRepresentation 方法来加载大文件,例如视频:

provider.loadFileRepresentation(forTypeIdentifier: "public.movie") { url, error in
  // do something with results
}

loadFileRepresentation 的使用方式与 UIImage 类似,但需要额外传入一个参数 forTypeIdentifier 来指定文件类型,指定为 public.movie 可以覆盖相册中的 .mov 和 .mp4 类型。

与照片不同的是,这个 api 返回的是一个 URL 类型的临时文件路径,苹果在这个 API 的说明中指出:系统会把请求的文件数据复制到这个路径对应的地址,并且在回调执行完毕后删除临时文件。

如果你需要在异步线程中对这个文件进行处理,那么需要再复制一次,将文件放到不会被系统自动删除的路径下,并且在处理完毕后自行删除。

关于 iCloud

iOS 相册提供了 iCloud 同步功能,如果用户开启了相册同步,那么相册中的照片、视频或者 LivePhoto 有可能会被上传到 iCloud,而本地只保存有缩略图,当请求某张照片时,相册会先从 iCloud 下载,然后再返回数据。

原先具有完整访问权限时,App 可以获得资源是否存在 iCloud 的状态,并且在下载时获得进度信息。由于 PHPicker 向 App 隐藏了所有隐私信息,因此我们无法再得知资源的 iCloud 同步状态,PHPicker 会自动从 iCloud 下载资源,并且完成之后通过 delegate 回调将数据返回。

不过这里有另外一个坑,不知道是因为工期问题还是苹果员工偷懒,目前宿主 App 是无法从 PHPicker 中获取到 iCloud 的下载进度信息的:

这是 PHPicker 的另一个大坑,这种情况下,如果用户选择了比较大的视频,或者是网络状态不好的话,视频导出依然需要耗费非常长的时间,并且没有进度信息!只能期待 iOS 的后续版本能够将将这一环补充完整了。

总结

PHPicker 的集成本身很简单,但是存在一些坑需要注意。简单总结一下优缺点:

优点

  • 保护用户隐私(初期可以作为 App 的宣传点?)
  • 不需要频繁询问相册权限,对于用户体验提升较大
  • 行为逻辑与系统相册保持一致,降低了用户的学习成本
  • 集成简单,在最低支持版本 iOS 14 之后,可以抛弃权限验证相关代码和第三方照片库(似乎很遥远)

缺点

  • 太过简单,缺少细节的筛选配置比如视频时长等
  • iCloud 大文件下载缺少进度信息(这个缺陷似乎把优点中的用户体验提升干掉了。。。)

iOS 14 正式版刚上线不久,目前还没有看到适配的大厂 App,可能也是在权衡利弊之中。不过从长远来看,PHPicker 必然会将那些第三方的相册组件扫进历史的垃圾堆的 :)


https://mp.weixin.qq.com/s/TOL1Ut2Qmn00xPxJGBd9Gg

iOS 性能优化:优化 App 启动速度

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

发布于:2天以前  |  23次阅读  |  详细内容 »

让你的应用远离越狱:iOS 14 App Attest 防护功能

当越狱在 iOS 设备第一次流行起来时,iOS 开发人员会尝试各种方法来保护自己的应用程序,以让应用免受盗版等不确定因素的困扰。有许多方法可以做到这一点,包括检查 Cydia 是否存在、检测应用程序是否可读取自身沙箱之外的文件、在检测到调试器时让应用程序崩溃等等。

发布于:9天以前  |  52次阅读  |  详细内容 »

探秘 iOS 14 的 WidgetKit

Widget Extension 提供了 small, medium, large 三个尺寸,不同尺寸可以展示不同的数据、不同的界面,开发者也可以锁定自己APP的 Widget 只有某类尺寸,相同的widget也能重复添加。作为添加在主屏幕上的控件,苹果用了 “At a glance” 来形容 widget ,所以 widget extension 是无法交互的,它能做的只有展示一些信息与点击两个作用,点击后就会引导至app,同时为了性能与耗电量的考虑,Widget extension 也不能展示视频和动态图像。

发布于:11天以前  |  75次阅读  |  详细内容 »

iOS14 Widget 万字指北,先人一步获得顶级流量

2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这次发布会上宣布了ARM架构Mac芯片(拳打Intel)、iOS 14 ATT(脚踢Facebook),可谓是一次载入史册(我是爸爸)的发布会了,当然还发布了被称为下一个顶级流量入口的Widget。踩着八月的尾巴,本次我们就来探究一下Widget。本文会从Widget初窥和Widget开发两个维度和章节来探究一下Widget, 其中初窥章节会带您简单的了解一下Widget,适合应用决策者阅读; 开发章节会带着您一步一步的完成设计开发Widget,适合程序员阅读。

发布于:11天以前  |  80次阅读  |  详细内容 »

OCRunner:完全体的iOS热修复方案

为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。

发布于:11天以前  |  74次阅读  |  详细内容 »

iOS APP图标版本化

在我们的项目开发过程中,需要频繁打包给测试人员去测试,有时候我们都不知道测试机上安装的版本是否是最新的,这样会造成很多不必要的麻烦和成本。因此我们需要将buildNumber以水印的方式打在APPIcon上,可以很直观的知道当前是哪一个版本。

发布于:11天以前  |  71次阅读  |  详细内容 »

百度App iOS工程化实践: EasyBox破冰之旅

百度App从单一的搜索工具发展到今天以搜索和Feed流为双引擎的综合性内容消费服务平台,其复杂程度已然不可同日而语矣。 作为一个日活过亿的超级App,业务规模庞大,相关技术人员超过千人,客户端支持主流的移动技术,涉及近百业务方,技术形态复杂,各种组件近三百个,代码百万量级,由此带来的工程化问题是技术团队的一个极大挑战。

发布于:11天以前  |  83次阅读  |  详细内容 »

百度APP iOS暗黑模式适配的完美解决方案

在2019WWDC的开场演讲中,苹果公布了即将推出的iOS13 DarkMode的新特性。此新特性不仅可以在夜晚保护视力,而且对于使用OLED的最新一代设备而言,也可以帮助用户节省电量消耗。不过此特性只支持iOS13以上的系统,为了给全系统所有用户最好的体验,研发出了一套皮肤主题框架,不仅可以全系统支持DarkMode,还可以扩展多套皮肤主题;

发布于:11天以前  |  76次阅读  |  详细内容 »

iOS 14 - 使用 PHPicker 选择照片和视频

绝大多数的 App 都要和相册打交道,选择照片或者视频,要么用来发个朋友圈,要么是放到什么地方做个背景。从 AssertLibrary 到 Photos 框架,苹果已经在多年之前就给相册相关的 API 做过一次大升级了。

发布于:13天以前  |  98次阅读  |  详细内容 »

iOS开发体验优化方案

随着Flutter等跨端框架的出现,业务开发同学经常需要在Android/iOS上跨端进行业务开发,问题定位等。新的不熟悉的环境的搭建总会遇到各种各样的问题,导致搭建失败,特别是iOS开发环境,是最复杂的,不仅环境搭建繁琐,而且切分支后的打包速度很慢,所以我们设计实现了两个工具,用于优化闲鱼iOS开发体验。

发布于:26天以前  |  126次阅读  |  详细内容 »

iOS14:再见了,“流氓”APP!

最近和苹果有关的重大消息可能就是从8月1日开始,AppStore中国区火速下架未获版号的游戏APP,数量超过30000款,之前小智就和大家说过,这未必不是一件好事,众多低质和“流氓”APP将被最大限度隔绝在iOS系统之外。

发布于:28天以前  |  176次阅读  |  详细内容 »

iOS 14 苹果对 Objective-C Runtime 的优化

Objective-C 是一门古老的语言,诞生于 1984 年,跟随 Apple 一路浮沉,见证了乔布斯创建了 NeXT,也见证了乔布斯重回 Apple 重创辉煌,它用它特立独行的语法,堆砌了 UIKit,AppKit, Foundation 等一个个基石,时间来到 2020 年,面对汹涌的"后浪" Swift,"老前辈" Objective-C 也在发挥着自己的余热,即使面对越来越多阵地失守,唯有“老兵不死,只会慢慢凋亡"才能体现的悲壮。今年,Apple 给 Objective-C Runtime 带来了新的优化,接下来,让我们深入理解这些变化。

发布于:1月以前  |  275次阅读  |  详细内容 »

iOS14 隐私适配及部分解决方案

在刚刚结束的线上 WWDC 2020 发布会上苹果向我们展示了新的 iOS14 系统。iOS14 的适配,很重要的一环就集中在用户隐私和安全方面。 在 iOS13 及以前,当用户首次访问应用程序时,会被要求开放大量权限,比如相册、定位、联系人,实际上该应用可能仅仅需要一个选择图片功能,却被要求开放整个照片库的权限,这确实是不合理的。对于相册,在 iOS14 中引入了 “LimitedPhotos Library” 的概念,用户可以授予应用访问其一部分的照片,对于应用来说,仅能读取到用户选择让应用来读取的照片,让我们看到了 Apple 对于用户隐私的尊重。这仅仅是一部分,在iOS14 中,可以看到诸多类似的保护用户隐私的措施,也需要我们升级适配。 最近在调研 iOS14的适配方案,本文主要分享一下 iOS14 上对于隐私授权的变更和部分适配方案,欢迎补充指正。

发布于:2月以前  |  324次阅读  |  详细内容 »

Metal新特性:大幅度提升iOS端性能

Metal 是一个和 OpenGL ES 类似的面向底层的图形编程接口,通过使用相关的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的时候发布。Metal 是 iOS 平台独有的,意味着它不能像 OpenGL ES 那样支持跨平台,但是它能最大的挖掘苹果移动设备的 GPU 能力,进行复杂的运算,像 Unity 等游戏引擎都通过 Metal 对 3D 能力进行了优化, App Store 还有相应的运用 Metal 技术的游戏专题。 闲鱼团队是比较早在客户端侧选择Flutter方案的技术团队,当前的闲鱼工程里也是一个较为复杂的Native-Flutter混合工程。作为一个2C的应用,性能和用户体验一直是闲鱼技术团队在开发中比较关注的点。而Metal这样的直接操作GPU的底层接口无疑会给闲鱼技术团队突破性能瓶颈提供一些新的思路。 下面会详细阐述一下这次大会Metal相关的新特性,以及对于闲鱼技术和整个淘系技术来说,这些新特性带来了哪些技术启发与思考。

发布于:2月以前  |  285次阅读  |  详细内容 »

最多阅读

快速配置 Sign In with Apple 1年以前  |  3180次阅读
给数组NSMutableArray排序 1年以前  |  2325次阅读
开篇 关于iOS越狱开发 1年以前  |  2322次阅读
APP适配iOS11 1年以前  |  2245次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2228次阅读
UITableViewCell高亮效果实现 1年以前  |  2159次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2075次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2042次阅读
所有iPhone设备尺寸汇总 1年以前  |  2011次阅读
关于Xcode不能打印崩溃日志 1年以前  |  1930次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  1926次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  1795次阅读
UIDevice的简单使用 1年以前  |  1658次阅读
为对象添加一个释放时触发的block 1年以前  |  1620次阅读
使用最高权限操作iPhone手机 1年以前  |  1561次阅读