视频链接:WWDC 2017 Session 505 - What’s New in Photos APIs

本节要介绍的是 Photos APIs 的一些新特性。简单的概括有下面这几点内容:

  • UIImagePickerController 的大幅优化
  • 授权模式的改进
  • 动图的支持
  • iCloud 照片图库的优化
  • 照片项目的扩展

后续内容,会对这几个点依次展开。

UIImagePickerController 的大幅优化

UIImagePickerController是系统提供的和相册及相机交互的一个类,通过这个类,你可以在应用中选择照片和视频。在 iOS 11 里,图片选择器有了许多的改进和新功能的引入。

隐私授权的改进

一直以来,Apple 十分关注用户的隐私安全。所以,之前在任何情况下,如果获取 Photos 资源,都需要获取用户的授权才可以进行。正如下面弹窗这样,请求用户的授权。

正因为授权过程的存在,使得应用程序与用户之间产生了矛盾。对于用户而言,需要他们打开一级隐私,这不是用户想要的;而对于应用程序来说,应用在未获取权限的情况下,无法执行相应的程序和操作,即便它自身有很多优秀的功能,都会因为未授权而无法使用。

在 iOS 11 中,如果通过UIImagePickerController 访问相册资源,这个警告弹窗不会再出现,会直接进行程序运行。看到这里,你或许会问:那用户的隐私保护怎么办?

首先需要介绍一下UIImagePickerController 新的授权模式。自 iOS 11 开始,UIImagePickerController成为了一个自动授权 API。也就是说,当应用程序要显示 API 的内容,将会是从一个自动处理的沙盒安全环境中获取,应用不再访问用户的Photo Library

并且,只有用户本人可以和UIImagePickerController UI进行互动。当用户做出一个选择,系统会取出选中的照片或视频,发送到应用中。这样就消除了前面提出的在应用中因为授权而产生的矛盾,同时这也让用户有了更高级别的隐私。因为不存在授权,也就不会再有请求授权。使用起来更为方便了。

元数据的获取更为方便

Photos 拥有丰富的元数据(metadata),内容包括创建日期、照片的格式,及一些其他不同类型的元数据。在 iOS 11 中,获取这些信息变得容易了很多。系统提供了一个全新的键值 UIImagePickerControllerImageURL,会包含所有UIImagePickerController的结果。我们可以使用这里的 URL,将对应数据读入应用并按照需要进行处理。该 URL 是文件 URL,指向一个应用中的临时文件,如果之后想对文件继续操作,建议把文件移动到更永久的文件路径中。

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {  
if let imageURL = info[UIImagePickerControllerImageURL] as? URL {
print(imageURL)
}
}

HEIF 图片格式的引入

iOS 11 中,Photos 引入了一种新的图片格式HEIF。同时,Apple 意识到生态系统完全接受HEIF需要一段时间,考虑到新类型图片格式的兼容性。Apple 为UIIMagePickerController提供了一个新属性imageExportPresent,让兼容过程变得更为容易。

var imageExportPreset: UIImagePickerControllerImportExportPreset { get set }

let imagePicker = UIImagePickerController()
imagePicker.imageExportPreset = .compatible
self.present(imagePicker, animated: true, completion: nil)

let imagePicker = UIImagePickerController()
imagePicker.imageExportPreset = .current
self.present(imagePicker, animated: true, completion: nil)

imageExportPresent拥有两种类型:

  • .compatible (兼容模式)
  • .current (当前模式)

compatible (兼容模式)下,如果用户选中的源图片是HEIF 格式,系统会通过转换,提供一个 JPEG 格式的图片。当然,JPEG 是该属性的默认值,如果不需要有什么改变,就不用再做更多的事情。

如果,需要获取的照片格式与拍摄时的格式相同,只需把属性值设为current (当前模式),这样就会得到与 Photo Library 里相同格式的图片,包括HEIF 格式

视频文件的获取更为方便

iOS 11 中,对视频选择的功能,也有了很好的改进。暂时把这部分内容放在一边,先来简单了解下AVFoundationAVFoundation是 Apple 提供的框架,用于丰富编辑照片播放。通过AVFoundation导出的素材可以拥有丰富的格式。

值得称赞的是,UIImagePickerController现在也有了类似的功能,引入了一个新属性videoExportPreset

var videoExportPreset: String { get set }

你可以通过这个方法来告诉系统,你所选中的视频需要以哪种格式返回。这样,你就可以轻松得到预设格式的资源内容了。

我们来看一个例子:

import AVFoundation
let imagePicker = UIImagePickerController()
imagePicker.videoExportPreset = AVAssetExportPresetHighestQuality self.present(imagePicker, animated: true, completion: nil)

如上代码中,首先,导入AVFoundation;接着,创建一个UIIMagePickerController实例,并描述我们想要资源文件以哪种格式返回(这里我们请求的是最高品质);之后显示选择器。

当用户做出选择时,无论是什么格式,系统都对其进行交叉编译,得到匹配格式,之后返回给用户。关于可用预设的完整清单,可以通过接口AVAssetExportSession查看。

照片和视频的保存有了新的隐私模式

前面,通过一些巧妙的设计,在保护用户隐私的情况下,已经实现了无缝选取。实际上,iOS 11 也对图片和视频的保存做了很多的优化。

在 iOS 11 中,保存一张照片或一段视频到用户的图片库中,系统提供了一个全新的安全模型及权限级别。UIImagePickerController对于保存图片资源视频资源分别提供了权限级别。一个是UIImageWriteToSavedPhotosAlbum,另一个是UISaveVideoAtPathToSavedPhotosAlbum

public func UIImageWriteToSavedPhotosAlbum(_ image: UIImage, _ completionTarget: Any?, _ completionSelector: Selector?, _ contextInfo: UnsafeMutableRawPointer?)


public func UISaveVideoAtPathToSavedPhotosAlbum(_ videoPath: String, _ completionTarget: Any?, _ completionSelector: Selector?, _ contextInfo: UnsafeMutableRawPointer?)

Add To Your Photos

这两种方式都只会请求添加授权,对于用户来说添加授权是很小的要求。因为这个权限只允许添加内容到用户的 Photo Library,而不涉及到读取权限。所以,很大程度上,用户会愿意给出这个权限。

PHAsset 获取的改进

我们来看一个例子:

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let asset = info[UIImagePickerControllerPHAsset] as? PHAsset {
print(asset)
}
}

上述代码中,我们实现了一个代理方法。在获得结果词典时,有了一个全新的键,键名为UIIMagePickerControllerPHAsset。取得该键的值,将会得到对应的资产对象,可以对其进行自由使用。

通过这些改变,增强了用户的隐私保护,也让UIIMagePickerController成为更强大而功能齐全的 API,满足了市面上大部分应用的需求。然而,有时会出现需要和照片框架进行更深入集成的需求,在这些场景下,Apple 推荐使用PhotoKit


PhotoKit

和照片相关的应用,一直以来是 App Store 里最受欢迎的一类。这一次,PhotoKit做了一些改进,可以让你写出拥有更棒用户体验的新功能。

Live Photo 介绍

首先一起了解下Live Photo的效果。Live Photo 效果包含:

  • 循环效果
  • 弹跳效果
  • 长曝光效果等

其中循环效果,是通过仔细分析视频帧,并无缝地和这些视频帧无止境循环缝合在一起;弹跳效果,它的工作原理和循环效果也是相似的;最后是长曝光效果,它将分析 Live Photo 的视频帧,创造令人惊艳的静物。

现存的PhotoKit媒体类型有这些:

enum PHAssetMediaType : Int {
case unknown
case image
case video
case audio
}

struct PHAssetMediaSubtype : OptionSet {
static var photoPanorama
static var photoHDR
static var photoScreenshot
static var photoLive
static var photoDepthEffect
static var videoStreamed
static var videoHighFrameRate
static var videoTimelapse
}

如果用户拍摄了一段视频,会想在应用中进行观看,并将拍摄的内容以视频方式使用。如果用户拍摄了一张Live Photo,同样会想要在应用中看到内容以Live Photo的方式呈现。为此,iOS 11 提供了三种媒体类型来实现对应目标:

  • image
  • video
  • photoLive

Live Photo效果比较复杂。为此,iOS 11 中引入了全新的PHAsset属性playbackStyle,让你可以简单实现Live Photo的播放。

class PHAsset : PHObject {
var playbackStyle: PHAssetPlaybackStyle { get }
}
enum PHAssetPlaybackStyle : Int {
case unsupported
case image
case imageAnimated
case livePhoto
case video
case videoLooping
}

playbackStyle属性,是唯一可以用来查看和决定要使用什么样的图片管理器 API、用什么样的视图来表现、以及为该视图设置什么样的 UI 限制。同时,Apple 更新了 PhotoKit 示例应用,包含所有这些新的播放风格。这里介绍下其中的三种,它们和前面提到的Live Photo 效果相关。从imageAnimated开始。

Animated Image

imageManager.requestImageData(for: asset, options: options) {
(data, dataUTI, orientation, info) in // 使用示例项目中的 animatedImageView
let animatedImage = AnimatedImage(data: data)
animatedImageView.animatedImage = animatedImage
}

iOS 11 有了一个期待已久的新功能。现在,在内置应用“照片”中支持了动画 GIF的播放。如果要在你的应用中播放 GIF,只需要从图像管理器请求图像数据,然后使用图像 IO 和 Core Graphics 进行播放。接下来是Live Photo

Live Photo

imageManager.requestLivePhoto(for: asset, targetSize: pixelSize, contentMode: .aspectFill, options: options) {
(livePhoto, info) in // 使用示例项目中的 PHLivePhotoView
livePhotoView.livePhoto = livePhoto
}

Live Photos一直很受用户的关注,如何在应用中更好地呈现它们,非常重要,也非常简单。在如上的这个例子里,首先从图像管理器请求一张Live Photo,之后设置PHLivePhotoView。在你的应用里,用户可以通过轻触播放一张Live Photo,正如用户在内置“照片”应用里的操作一样。

Looping Video

swiftimageManager.requestPlayerItem(forVideo: asset, options: options) { 
playerItem, info in
DispatchQueue.main.async {
let player = AVQueuePlayer()
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
playerLayer.player = player
player.play()
}}

在今年所推出的视频循环中,既包括弹跳效果,也包括Live Photo的循环效果。现在,在你的应用里播放这些和播放普通视频非常相似。可以请求播放器项目,并使用AVFoundation播放,还可以使用AVPlayerLooper取得循环效果。 可见,表现用户的媒体变得更为轻便,以他们真正想表现的方式,你也可以在自己的应用中,对这些全新的媒体类型,更为创新地表现。

iCloud 照片图库的改进

“iCloud 照片图库”可以与“照片”应用完美搭配使用。当用户开启“iCloud 照片图库”时,用户的照片和视频会被安全上传到 iCloud 中,同时这些更改会同步到用户的其他设备中。自动上传 iCloud 操作的触发条件是,设备连接到 Wi-Fi 且电量充足。根据用户的网络情况,在所有设备和 iCloud.com 上看到同步照片和视频所需的时间可能会不同。

使用 iPhone 拍照的用户,也常会使用“照片”相关的第三方应用。这些用户,大致可以分为 3 类:轻度用户、中度爱好者和重度专业用户。对于重度用户而言,由于自身图库中有很多内容,在第一次使用应用时,需要加载大批量的照片,这个过程中会十分耗时。而且这种耗时的加载状态,会对应用的用户体验大打折扣。

在 iOS 11 中,针对如何更快速高效地操作“大型照片图库”这一点做了优化,后面的内容会依次展开描述。

创建一个用于测试的“大型照片图库”

前面提到,如果图库中有大量内容,在应用加载数据时,会处于长时间的加载状态。而你如果想创建一个拥有大批量图片的图库,还是有些难度的。友好的是,Apple 为开发者提供了一个用户创建图库的示例应用:Photo Library Filler 。下载该应用并安装到测试设备,点击“Add Photos” 按钮,它便会迅速生成一个拥有大批量图片的图库供测试使用。

到这里,你就拥有了一个可用于测试的“大型照片图库”

现在,如何从“照片图库”提取图片

看下面这段代码:” value=”本节要介绍的是 Photos APIs 的一些新特性。简单的概括有下面这几点内容:

  • UIImagePickerController 的大幅优化
  • 授权模式的改进
  • 动图的支持
  • iCloud 照片图库的优化
  • 照片项目的扩展

后续内容,会对这几个点依次展开。

UIImagePickerController 的大幅优化

UIImagePickerController是系统提供的和相册及相机交互的一个类,通过这个类,你可以在应用中选择照片和视频。在 iOS 11 里,图片选择器有了许多的改进和新功能的引入。

隐私授权的改进

一直以来,Apple 十分关注用户的隐私安全。所以,之前在任何情况下,如果获取 Photos 资源,都需要获取用户的授权才可以进行。正如下面弹窗这样,请求用户的授权。

正因为授权过程的存在,使得应用程序与用户之间产生了矛盾。对于用户而言,需要他们打开一级隐私,这不是用户想要的;而对于应用程序来说,应用在未获取权限的情况下,无法执行相应的程序和操作,即便它自身有很多优秀的功能,都会因为未授权而无法使用。

在 iOS 11 中,如果通过UIImagePickerController 访问相册资源,这个警告弹窗不会再出现,会直接进行程序运行。看到这里,你或许会问:那用户的隐私保护怎么办?

首先需要介绍一下UIImagePickerController 新的授权模式。自 iOS 11 开始,UIImagePickerController成为了一个自动授权 API。也就是说,当应用程序要显示 API 的内容,将会是从一个自动处理的沙盒安全环境中获取,应用不再访问用户的Photo Library

并且,只有用户本人可以和UIImagePickerController UI进行互动。当用户做出一个选择,系统会取出选中的照片或视频,发送到应用中。这样就消除了前面提出的在应用中因为授权而产生的矛盾,同时这也让用户有了更高级别的隐私。因为不存在授权,也就不会再有请求授权。使用起来更为方便了。

元数据的获取更为方便

Photos 拥有丰富的元数据(metadata),内容包括创建日期、照片的格式,及一些其他不同类型的元数据。在 iOS 11 中,获取这些信息变得容易了很多。系统提供了一个全新的键值 UIImagePickerControllerImageURL,会包含所有UIImagePickerController的结果。我们可以使用这里的 URL,将对应数据读入应用并按照需要进行处理。该 URL 是文件 URL,指向一个应用中的临时文件,如果之后想对文件继续操作,建议把文件移动到更永久的文件路径中。

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {  
if let imageURL = info[UIImagePickerControllerImageURL] as? URL {
print(imageURL)
}
}

HEIF 图片格式的引入

iOS 11 中,Photos 引入了一种新的图片格式HEIF。同时,Apple 意识到生态系统完全接受HEIF需要一段时间,考虑到新类型图片格式的兼容性。Apple 为UIIMagePickerController提供了一个新属性imageExportPresent,让兼容过程变得更为容易。

var imageExportPreset: UIImagePickerControllerImportExportPreset { get set }

let imagePicker = UIImagePickerController()
imagePicker.imageExportPreset = .compatible
self.present(imagePicker, animated: true, completion: nil)

let imagePicker = UIImagePickerController()
imagePicker.imageExportPreset = .current
self.present(imagePicker, animated: true, completion: nil)

imageExportPresent拥有两种类型:

  • .compatible (兼容模式)
  • .current (当前模式)

compatible (兼容模式)下,如果用户选中的源图片是HEIF 格式,系统会通过转换,提供一个 JPEG 格式的图片。当然,JPEG 是该属性的默认值,如果不需要有什么改变,就不用再做更多的事情。

如果,需要获取的照片格式与拍摄时的格式相同,只需把属性值设为current (当前模式),这样就会得到与 Photo Library 里相同格式的图片,包括HEIF 格式

视频文件的获取更为方便

iOS 11 中,对视频选择的功能,也有了很好的改进。暂时把这部分内容放在一边,先来简单了解下AVFoundationAVFoundation是 Apple 提供的框架,用于丰富编辑照片播放。通过AVFoundation导出的素材可以拥有丰富的格式。

值得称赞的是,UIImagePickerController现在也有了类似的功能,引入了一个新属性videoExportPreset

var videoExportPreset: String { get set }

你可以通过这个方法来告诉系统,你所选中的视频需要以哪种格式返回。这样,你就可以轻松得到预设格式的资源内容了。

我们来看一个例子:

import AVFoundation
let imagePicker = UIImagePickerController()
imagePicker.videoExportPreset = AVAssetExportPresetHighestQuality self.present(imagePicker, animated: true, completion: nil)

如上代码中,首先,导入AVFoundation;接着,创建一个UIIMagePickerController实例,并描述我们想要资源文件以哪种格式返回(这里我们请求的是最高品质);之后显示选择器。

当用户做出选择时,无论是什么格式,系统都对其进行交叉编译,得到匹配格式,之后返回给用户。关于可用预设的完整清单,可以通过接口AVAssetExportSession查看。

照片和视频的保存有了新的隐私模式

前面,通过一些巧妙的设计,在保护用户隐私的情况下,已经实现了无缝选取。实际上,iOS 11 也对图片和视频的保存做了很多的优化。

在 iOS 11 中,保存一张照片或一段视频到用户的图片库中,系统提供了一个全新的安全模型及权限级别。UIImagePickerController对于保存图片资源视频资源分别提供了权限级别。一个是UIImageWriteToSavedPhotosAlbum,另一个是UISaveVideoAtPathToSavedPhotosAlbum

public func UIImageWriteToSavedPhotosAlbum(_ image: UIImage, _ completionTarget: Any?, _ completionSelector: Selector?, _ contextInfo: UnsafeMutableRawPointer?)


public func UISaveVideoAtPathToSavedPhotosAlbum(_ videoPath: String, _ completionTarget: Any?, _ completionSelector: Selector?, _ contextInfo: UnsafeMutableRawPointer?)

Add To Your Photos

这两种方式都只会请求添加授权,对于用户来说添加授权是很小的要求。因为这个权限只允许添加内容到用户的 Photo Library,而不涉及到读取权限。所以,很大程度上,用户会愿意给出这个权限。

PHAsset 获取的改进

我们来看一个例子:

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let asset = info[UIImagePickerControllerPHAsset] as? PHAsset {
print(asset)
}
}

上述代码中,我们实现了一个代理方法。在获得结果词典时,有了一个全新的键,键名为UIIMagePickerControllerPHAsset。取得该键的值,将会得到对应的资产对象,可以对其进行自由使用。

通过这些改变,增强了用户的隐私保护,也让UIIMagePickerController成为更强大而功能齐全的 API,满足了市面上大部分应用的需求。然而,有时会出现需要和照片框架进行更深入集成的需求,在这些场景下,Apple 推荐使用PhotoKit


PhotoKit

和照片相关的应用,一直以来是 App Store 里最受欢迎的一类。这一次,PhotoKit做了一些改进,可以让你写出拥有更棒用户体验的新功能。

Live Photo 介绍

首先一起了解下Live Photo的效果。Live Photo 效果包含:

  • 循环效果
  • 弹跳效果
  • 长曝光效果等

其中循环效果,是通过仔细分析视频帧,并无缝地和这些视频帧无止境循环缝合在一起;弹跳效果,它的工作原理和循环效果也是相似的;最后是长曝光效果,它将分析 Live Photo 的视频帧,创造令人惊艳的静物。

现存的PhotoKit媒体类型有这些:

enum PHAssetMediaType : Int {
case unknown
case image
case video
case audio
}

struct PHAssetMediaSubtype : OptionSet {
static var photoPanorama
static var photoHDR
static var photoScreenshot
static var photoLive
static var photoDepthEffect
static var videoStreamed
static var videoHighFrameRate
static var videoTimelapse
}

如果用户拍摄了一段视频,会想在应用中进行观看,并将拍摄的内容以视频方式使用。如果用户拍摄了一张Live Photo,同样会想要在应用中看到内容以Live Photo的方式呈现。为此,iOS 11 提供了三种媒体类型来实现对应目标:

  • image
  • video
  • photoLive

Live Photo效果比较复杂。为此,iOS 11 中引入了全新的PHAsset属性playbackStyle,让你可以简单实现Live Photo的播放。

class PHAsset : PHObject {
var playbackStyle: PHAssetPlaybackStyle { get }
}
enum PHAssetPlaybackStyle : Int {
case unsupported
case image
case imageAnimated
case livePhoto
case video
case videoLooping
}

playbackStyle属性,是唯一可以用来查看和决定要使用什么样的图片管理器 API、用什么样的视图来表现、以及为该视图设置什么样的 UI 限制。同时,Apple 更新了 PhotoKit 示例应用,包含所有这些新的播放风格。这里介绍下其中的三种,它们和前面提到的Live Photo 效果相关。从imageAnimated开始。

Animated Image

imageManager.requestImageData(for: asset, options: options) {
(data, dataUTI, orientation, info) in // 使用示例项目中的 animatedImageView
let animatedImage = AnimatedImage(data: data)
animatedImageView.animatedImage = animatedImage
}

iOS 11 有了一个期待已久的新功能。现在,在内置应用“照片”中支持了动画 GIF的播放。如果要在你的应用中播放 GIF,只需要从图像管理器请求图像数据,然后使用图像 IO 和 Core Graphics 进行播放。接下来是Live Photo

Live Photo

imageManager.requestLivePhoto(for: asset, targetSize: pixelSize, contentMode: .aspectFill, options: options) {
(livePhoto, info) in // 使用示例项目中的 PHLivePhotoView
livePhotoView.livePhoto = livePhoto
}

Live Photos一直很受用户的关注,如何在应用中更好地呈现它们,非常重要,也非常简单。在如上的这个例子里,首先从图像管理器请求一张Live Photo,之后设置PHLivePhotoView。在你的应用里,用户可以通过轻触播放一张Live Photo,正如用户在内置“照片”应用里的操作一样。

Looping Video

swiftimageManager.requestPlayerItem(forVideo: asset, options: options) { 
playerItem, info in
DispatchQueue.main.async {
let player = AVQueuePlayer()
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
playerLayer.player = player
player.play()
}}

在今年所推出的视频循环中,既包括弹跳效果,也包括Live Photo的循环效果。现在,在你的应用里播放这些和播放普通视频非常相似。可以请求播放器项目,并使用AVFoundation播放,还可以使用AVPlayerLooper取得循环效果。 可见,表现用户的媒体变得更为轻便,以他们真正想表现的方式,你也可以在自己的应用中,对这些全新的媒体类型,更为创新地表现。

iCloud 照片图库的改进

“iCloud 照片图库”可以与“照片”应用完美搭配使用。当用户开启“iCloud 照片图库”时,用户的照片和视频会被安全上传到 iCloud 中,同时这些更改会同步到用户的其他设备中。自动上传 iCloud 操作的触发条件是,设备连接到 Wi-Fi 且电量充足。根据用户的网络情况,在所有设备和 iCloud.com 上看到同步照片和视频所需的时间可能会不同。

使用 iPhone 拍照的用户,也常会使用“照片”相关的第三方应用。这些用户,大致可以分为 3 类:轻度用户、中度爱好者和重度专业用户。对于重度用户而言,由于自身图库中有很多内容,在第一次使用应用时,需要加载大批量的照片,这个过程中会十分耗时。而且这种耗时的加载状态,会对应用的用户体验大打折扣。

在 iOS 11 中,针对如何更快速高效地操作“大型照片图库”这一点做了优化,后面的内容会依次展开描述。

创建一个用于测试的“大型照片图库”

前面提到,如果图库中有大量内容,在应用加载数据时,会处于长时间的加载状态。而你如果想创建一个拥有大批量图片的图库,还是有些难度的。友好的是,Apple 为开发者提供了一个用户创建图库的示例应用:Photo Library Filler 。下载该应用并安装到测试设备,点击“Add Photos” 按钮,它便会迅速生成一个拥有大批量图片的图库供测试使用。

到这里,你就拥有了一个可用于测试的“大型照片图库”

现在,如何从“照片图库”提取图片

看下面这段代码:

let assets = PHAsset.fetchAssets(with: options)

这个方法用于从用户的“照片图库”提取图片,等号右侧用于提取资产,左侧为提取结果

let options = PHFetchOptions()options.predicate = NSPredicate(format: "isFavorite = %d", true)
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let assets = PHAsset.fetchAssets(with: options)

在上述代码中,首先提取库里所有的 Asset,并进行筛选,筛选条件为isFavorite = true,之后按照对应的创建日期进行排序。这时,如果在这些自定义提取里发现耗时,那么简化这里的筛选条件会十分必要。不同的筛选方式,可能会意味着查询耗时的巨大差异。造成这种差异的原因是你的操作可能在数据库优化路径之外进行,同时又试图回到优化路径中,这样就产生了不同的耗时差距。

比这种自定义提取更好的是,尽可能避免这种操作。例如下面这个例子中,我们实际上提取的是用户最喜欢智能相册。然后在智能相册里提取 Asset。这样既可以使用已有的关键字排序描述符,还可以保证操作是在数据库优化路径中进行的。

let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: nil)
let assets = PHAsset.fetchAssets(in: smartAlbums.firstObject, options: nil)

接着,看下返回结果。返回对象是一个PHFetchResult类型的对象。PHFetchResult 类型非常像一个数列,并且可以像数组一样来使用。但从内部实现来看,PHFetchResult和数列的工作机制还是完全不同的。并且这也是PhotoKit在大型图库操作方面,能够如此快速高效的原因之一。

我们来看看它的内部工作机制。

最开始,它只包含一个标识符列表。这意味着可以迅速返回对应的 Asset。但开始使用后,有更多工作必须执行。我们以一个枚举作为例子。

在这里,我们从索引 0 开始枚举。目前只有一个标识符,你还需要从数据库里提取元数据。为此创建一个PHAsset 对象,以便将 Asset 的元数据返回给你。

实际上,同时也创建了一个批处理

当我们继续枚举时,索引 1 和 2实际已经在内存中了。枚举继续,它将访问硬盘,获取后续 Asset 的元数据。

在提取结果量级较小的情况下,这样的提取,并不会有太大的影响。但如果提取结果包含10万个 Asset。其中,每一批都需要占用几 kb 内存。如果是10w 批,那将会产生几百兆的内存用量。更糟糕的是,每一批都需要几毫秒的提取时间,如果有10w 批,就需要消耗 10s来枚举这样的一个大型提取结果。所以,应该尽量避免枚举操作

在 PHFetchResult 中查询 Asset 更优的方式

实际开发过程中,枚举操作总是可能会出现的。这里列举一个例子。现在,你需要从一个提取结果中查询一个Asset 的索引

第一种方式,可以通过枚举该提取结果,通过“等于”比较返回的对象,来获取对应Asset 的索引。但是,枚举会非常耗时,所以更好的方法是通过另一种方式,使用高端 API

如上,通过使用indexOfObject来进行 Asset 索引的查询。而indexOfObject方法内部是通过比较“对象标识符”,以找到符合条件的 Asset,这样就不会有附加的“硬盘访问”和“数据库提取”。进而避免了第一种方式中,因为枚举出现的耗时操作。同样的,对containtsObject也是如此。

照片项目的拓展

一直以来,Apple 允许用户围绕照片创建丰富的有创意的项目。

PHProjectExtensionController 的引入

现在,“照片”中添加了一个新的扩展。对应的,Xcode中也添加了一个新模板,开发者在自己的应用里可以轻松创建这些扩展。此外,“照片”应用会自动发现你的拓展,大大提高了扩展应用被用户知道的概率。不仅如此,Apple 为了让这些扩展更容易为用户所发现,给这些扩展应用提供了App Store的直接链接。该链接会打开App Store窗口,并自动显示支持该扩展的应用。


扩展只存在于开发者的应用内。对于开发者来说,如果你在 App Store 已经有了一个关于 Photos 相关的应用。此时,就可以将扩展代码移动到该扩展空间内,并加以利用。之后,添加一个视图控制器,并实现PHProjectExtensionController协议。一切就位,“照片应用”便可以发现你的扩展了。

在用户选择“扩展应用”,并用它创建一个项目时,"照片应用"会发送一些字节数据(PHProjectExtensionContextPHProjectInfo)到对应的“扩展应用”。之后“照片应用”得到相应的返回结果,知道可以安装你的视图控制器。

过程中遵循的协议本身,对支持的项目类型有一个可选属性,可用于快速描述想让用户选择的选项。

optional public var supportedProjectTypes: [PHProjectTypeDescription] { get }

在实际使用过程中,用户既可以选择退出,也可以选择直接进入扩展。这些,在视图控制器里也有特定的函数方法。

public protocol PHProjectExtensionController : NSObjectProtocol {
//第一次使用该扩展创建项目时调用
public func beginProject(with extensionContext: PHProjectExtensionContext,
projectInfo: PHProjectInfo, completion: @escaping (Error?) -> Void)

//每次用户回到以前创建的项目时调用
public func resumeProject(with extensionContext: PHProjectExtensionContext,
completion: @escaping (Error?) -> Void)

//用户离开项目时调用
public func finishProject(completionHandler completion: @escaping () -> Void)
}

通过第一个函数方法,我们可以得到上下文及项目详细。在第一次使用该扩展创建项目时,会调用该方法。

第二个函数方法,我们同样可以获得上下文。在每次用户回到扩展项目时,会调用该方法。

最后一个函数方法。如果用户在扩展项目内,当他们决定切换离开,会调用该函数。通过回调,你可以清理任何正在处理的数据,或是关闭任何让处理器忙碌的任务,或是正在执行的动画。

PHProjectExtensionContext 是什么


PHProjectExtensionContext这个容器里,包含两个非常重要的对象。一个是PHProject,另一个是PHPhotoLibrary

PHProject 介绍

// PHProject.h 
// Photos
class PHProject : PHAssetCollection { var projectExtensionData: Data { get }
}

PHProject本身只是PHAsset的一个子集。在子集中,创建PHProject,只添加了一个非常重要的属性projectExtensionData。可以用于保存任何你需要的数据,它是你正在使用的资产标识符的列表。也许是一些基本的布局信息配置信息。同时属性projectExtensionData并不是为了照片缓存、缩略图之类的存在。因为这些功能,你可以快速地创建或把它们缓存到其他位置。为了它小而有用,抛开了这些功能,并且projectExtensionData被限制定为1 兆。因为这里面的信息,是一系列的字符串,所以1 兆大小已经足够了。即使用户不断创建项目,也不会增大用户的库。

PHProjectChangeRequest

do {
let changeRequest = PHProjectChangeRequest(project: self.project)
try self.library.performChangesAndWait {
changeRequest.projectExtensionData = NSKeyedArchiver.archivedData(withRootObject: cloudIdentifiers)
}} catch {
print("Failed to save project data: \(error.localizedDescription)")
}

设置数据非常简单。只需按上述实例化,实例化后,可以在Photo Library调用performChangesAndWait函数,在里面将数据设置成任何你想要的样式。

PHProjectInfo 介绍

最高层的ProductInfo分为下面这几个区:

当看到这个构造时,可能会问,为什么数组里面还有数组。为什么是这种嵌套结构。但是如果想想“照片”应用里的“回忆”功能,会发现这些是有道理的。

“回忆”本身建立于大量资产之上,允许用户回忆时,可以切换“显示照片摘要”“显示所有照片”。通过下面的一张图解,可以更清晰地描述为什么使用数组。

Section Contents数组是已排序数组。索引为 0的对象是最优内容,是资产集合最精炼的摘要;而数组末端的对象内容是最多的,包含了所有的照片数据。开发过程中,开发者需要根据具体的需求,选择性地使用。

PHCloudIdentifier 介绍

PHCloudIdentifier是一个全新的概念。当你想把数据存到用户的Photo Library时,数据可能会被同步到用户其他的设备中。为了确保在保存的数据,合理地同步到其他设备中,iOS 11 推出了一个新对象PHCloudIdentifier

//获取当前 Cloud Identifiers
cloudIdentifiers += dataDict.value(forKey: "contentIdentifiers") as! [PHCloudIdentifier]

//转换为 Local Identifiers
let localIdentifiers = self.library.localIdentifiers(for: cloudIdentifiers)

可以将它看做资产的全局标识符,但也并不像全局字符串那么简单,因为还需要处理同步和同步状态等情况。而这些复杂操作,系统已经为我们做了。你必须要做的唯一操作是,在提取之前,确保你的转换总是从全局标识符本地标识符。可以通过PHPhotoLibrary里的方法来进行双向转换。

关于“视图布局”的改进

例如上述这种网格布局,对用户来说是很愉快的。如果开发者可以直接访问这个布局,不是会很棒吗?在 iOS 11 中,你确实可以进行访问了。

为了支持访问,系统首先确定了一个坐标系。

如果你查看“回忆”功能,会发现所有内容都被排列在一个由 4x3 单元格构成的网络中。但它是一个非正方形尺寸,不利于拓展。所以又有了下面这样的结构。

例如上述实例图片的的布局,可以被转化为一个由 20 个统一列组成的网络空间。确定了这个坐标系,就可以根据需求任意缩放。

并且,通过这个坐标系,也可以和系统互相传递坐标信息。例如上图的坐标为(0, 0, 8 , 9)

PHProjectElement 介绍

// PHProjectElementclass 
PHProjectElement : NSObject, NSSecureCoding {
//权重的范围是 0.0 - 1.0,默认为 0.5
var weight: Double { get }
//元素在网络布局中的坐标
var placement: CGRect { get }
}

Section Content里,系统提供了一组元素。所有元素都是PHProjectElement的子集。这里有两个非常重要的属性:

  • placement(位置)
  • weight(权重)

“位置”属性,前面已经有所介绍了,这里介绍下“权重”属性。再次回到“回忆”功能,在大量资产中如果想确定最相关的照片,系统需要有自己的评分系统。这里,评分系统通过给每个元素一个权重值,来代表每个元素的重要性。权重值从0.01.0,默认值是0.5,也就是说,权重值为0.5的资产代表普通。

RegionsOfInterest 是什么

var regionsOfInterest: [PHProjectRegionOfInterest] { get }

这里,有这样的一个概念,称为“兴趣区”。也就是这里的regionsOfInterest属性,这是PHProjectAssetElement所特有的。

macOS API里,已经有很多方法,可以用来进行面部识别,从而寻找图片中的脸。但从这些方法无法知道这些脸的相关性。来看下面一副示例图:

你会注意到,这些脸部有对应的标识符。在不同的照片里相同的脸,会看到被标记为相同的标识符。这样的表示十分有趣。如果你正在处理动画、幻灯片等效果,这将非常有用。因为你现在可以真正把大集合中的图片位置彼此联系起来了。对于体验上的改进来说,这会是一个很棒的属性。

总结

本节主要介绍了 Photos APIs 的新特性,主要包含了以下几点内容:

  • 改进的授权模式
  • 大幅优化的 UIImagePickerController
  • 全新的图片格式 HEIF
  • 大型图片库的创建
  • 及为 Photos 创建项目扩展

回头来看开篇提出的三点疑问:

  • 如何以一种不违反用户信任的方式获取及保存内容到相册?
  • 是否可以为 Photo Library 创建扩展内容?
  • 如何在应用中简单、高效地实现这些操作?

这些,在新的 Photos APIs 里都有了相应的解决方案。综上可以看到 Photos 也在越来越完善,可扩展性越来越强,功能也在越来越强大。

相关资料

“ link_users=”{}” data-body=”本节要介绍的是 Photos APIs 的一些新特性。简单的概括有下面这几点内容:

  • UIImagePickerController 的大幅优化
  • 授权模式的改进
  • 动图的支持
  • iCloud 照片图库的优化
  • 照片项目的扩展

后续内容,会对这几个点依次展开。

UIImagePickerController 的大幅优化

UIImagePickerController是系统提供的和相册及相机交互的一个类,通过这个类,你可以在应用中选择照片和视频。在 iOS 11 里,图片选择器有了许多的改进和新功能的引入。

隐私授权的改进

一直以来,Apple 十分关注用户的隐私安全。所以,之前在任何情况下,如果获取 Photos 资源,都需要获取用户的授权才可以进行。正如下面弹窗这样,请求用户的授权。

正因为授权过程的存在,使得应用程序与用户之间产生了矛盾。对于用户而言,需要他们打开一级隐私,这不是用户想要的;而对于应用程序来说,应用在未获取权限的情况下,无法执行相应的程序和操作,即便它自身有很多优秀的功能,都会因为未授权而无法使用。

在 iOS 11 中,如果通过UIImagePickerController 访问相册资源,这个警告弹窗不会再出现,会直接进行程序运行。看到这里,你或许会问:那用户的隐私保护怎么办?

首先需要介绍一下UIImagePickerController 新的授权模式。自 iOS 11 开始,UIImagePickerController成为了一个自动授权 API。也就是说,当应用程序要显示 API 的内容,将会是从一个自动处理的沙盒安全环境中获取,应用不再访问用户的Photo Library

并且,只有用户本人可以和UIImagePickerController UI进行互动。当用户做出一个选择,系统会取出选中的照片或视频,发送到应用中。这样就消除了前面提出的在应用中因为授权而产生的矛盾,同时这也让用户有了更高级别的隐私。因为不存在授权,也就不会再有请求授权。使用起来更为方便了。

元数据的获取更为方便

Photos 拥有丰富的元数据(metadata),内容包括创建日期、照片的格式,及一些其他不同类型的元数据。在 iOS 11 中,获取这些信息变得容易了很多。系统提供了一个全新的键值 UIImagePickerControllerImageURL,会包含所有UIImagePickerController的结果。我们可以使用这里的 URL,将对应数据读入应用并按照需要进行处理。该 URL 是文件 URL,指向一个应用中的临时文件,如果之后想对文件继续操作,建议把文件移动到更永久的文件路径中。

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {  
if let imageURL = info[UIImagePickerControllerImageURL] as? URL {
print(imageURL)
}
}

HEIF 图片格式的引入

iOS 11 中,Photos 引入了一种新的图片格式HEIF。同时,Apple 意识到生态系统完全接受HEIF需要一段时间,考虑到新类型图片格式的兼容性。Apple 为UIIMagePickerController提供了一个新属性imageExportPresent,让兼容过程变得更为容易。

var imageExportPreset: UIImagePickerControllerImportExportPreset { get set }

let imagePicker = UIImagePickerController()
imagePicker.imageExportPreset = .compatible
self.present(imagePicker, animated: true, completion: nil)

let imagePicker = UIImagePickerController()
imagePicker.imageExportPreset = .current
self.present(imagePicker, animated: true, completion: nil)

imageExportPresent拥有两种类型:

  • .compatible (兼容模式)
  • .current (当前模式)

compatible (兼容模式)下,如果用户选中的源图片是HEIF 格式,系统会通过转换,提供一个 JPEG 格式的图片。当然,JPEG 是该属性的默认值,如果不需要有什么改变,就不用再做更多的事情。

如果,需要获取的照片格式与拍摄时的格式相同,只需把属性值设为current (当前模式),这样就会得到与 Photo Library 里相同格式的图片,包括HEIF 格式

视频文件的获取更为方便

iOS 11 中,对视频选择的功能,也有了很好的改进。暂时把这部分内容放在一边,先来简单了解下AVFoundationAVFoundation是 Apple 提供的框架,用于丰富编辑照片播放。通过AVFoundation导出的素材可以拥有丰富的格式。

值得称赞的是,UIImagePickerController现在也有了类似的功能,引入了一个新属性videoExportPreset

var videoExportPreset: String { get set }

你可以通过这个方法来告诉系统,你所选中的视频需要以哪种格式返回。这样,你就可以轻松得到预设格式的资源内容了。

我们来看一个例子:

import AVFoundation
let imagePicker = UIImagePickerController()
imagePicker.videoExportPreset = AVAssetExportPresetHighestQuality self.present(imagePicker, animated: true, completion: nil)

如上代码中,首先,导入AVFoundation;接着,创建一个UIIMagePickerController实例,并描述我们想要资源文件以哪种格式返回(这里我们请求的是最高品质);之后显示选择器。

当用户做出选择时,无论是什么格式,系统都对其进行交叉编译,得到匹配格式,之后返回给用户。关于可用预设的完整清单,可以通过接口AVAssetExportSession查看。

照片和视频的保存有了新的隐私模式

前面,通过一些巧妙的设计,在保护用户隐私的情况下,已经实现了无缝选取。实际上,iOS 11 也对图片和视频的保存做了很多的优化。

在 iOS 11 中,保存一张照片或一段视频到用户的图片库中,系统提供了一个全新的安全模型及权限级别。UIImagePickerController对于保存图片资源视频资源分别提供了权限级别。一个是UIImageWriteToSavedPhotosAlbum,另一个是UISaveVideoAtPathToSavedPhotosAlbum

public func UIImageWriteToSavedPhotosAlbum(_ image: UIImage, _ completionTarget: Any?, _ completionSelector: Selector?, _ contextInfo: UnsafeMutableRawPointer?)


public func UISaveVideoAtPathToSavedPhotosAlbum(_ videoPath: String, _ completionTarget: Any?, _ completionSelector: Selector?, _ contextInfo: UnsafeMutableRawPointer?)

Add To Your Photos

这两种方式都只会请求添加授权,对于用户来说添加授权是很小的要求。因为这个权限只允许添加内容到用户的 Photo Library,而不涉及到读取权限。所以,很大程度上,用户会愿意给出这个权限。

PHAsset 获取的改进

我们来看一个例子:

public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let asset = info[UIImagePickerControllerPHAsset] as? PHAsset {
print(asset)
}
}

上述代码中,我们实现了一个代理方法。在获得结果词典时,有了一个全新的键,键名为UIIMagePickerControllerPHAsset。取得该键的值,将会得到对应的资产对象,可以对其进行自由使用。

通过这些改变,增强了用户的隐私保护,也让UIIMagePickerController成为更强大而功能齐全的 API,满足了市面上大部分应用的需求。然而,有时会出现需要和照片框架进行更深入集成的需求,在这些场景下,Apple 推荐使用PhotoKit


PhotoKit

和照片相关的应用,一直以来是 App Store 里最受欢迎的一类。这一次,PhotoKit做了一些改进,可以让你写出拥有更棒用户体验的新功能。

Live Photo 介绍

首先一起了解下Live Photo的效果。Live Photo 效果包含:

  • 循环效果
  • 弹跳效果
  • 长曝光效果等

其中循环效果,是通过仔细分析视频帧,并无缝地和这些视频帧无止境循环缝合在一起;弹跳效果,它的工作原理和循环效果也是相似的;最后是长曝光效果,它将分析 Live Photo 的视频帧,创造令人惊艳的静物。

现存的PhotoKit媒体类型有这些:

enum PHAssetMediaType : Int {
case unknown
case image
case video
case audio
}

struct PHAssetMediaSubtype : OptionSet {
static var photoPanorama
static var photoHDR
static var photoScreenshot
static var photoLive
static var photoDepthEffect
static var videoStreamed
static var videoHighFrameRate
static var videoTimelapse
}

如果用户拍摄了一段视频,会想在应用中进行观看,并将拍摄的内容以视频方式使用。如果用户拍摄了一张Live Photo,同样会想要在应用中看到内容以Live Photo的方式呈现。为此,iOS 11 提供了三种媒体类型来实现对应目标:

  • image
  • video
  • photoLive

Live Photo效果比较复杂。为此,iOS 11 中引入了全新的PHAsset属性playbackStyle,让你可以简单实现Live Photo的播放。

class PHAsset : PHObject {
var playbackStyle: PHAssetPlaybackStyle { get }
}
enum PHAssetPlaybackStyle : Int {
case unsupported
case image
case imageAnimated
case livePhoto
case video
case videoLooping
}

playbackStyle属性,是唯一可以用来查看和决定要使用什么样的图片管理器 API、用什么样的视图来表现、以及为该视图设置什么样的 UI 限制。同时,Apple 更新了 PhotoKit 示例应用,包含所有这些新的播放风格。这里介绍下其中的三种,它们和前面提到的Live Photo 效果相关。从imageAnimated开始。

Animated Image

imageManager.requestImageData(for: asset, options: options) {
(data, dataUTI, orientation, info) in // 使用示例项目中的 animatedImageView
let animatedImage = AnimatedImage(data: data)
animatedImageView.animatedImage = animatedImage
}

iOS 11 有了一个期待已久的新功能。现在,在内置应用“照片”中支持了动画 GIF的播放。如果要在你的应用中播放 GIF,只需要从图像管理器请求图像数据,然后使用图像 IO 和 Core Graphics 进行播放。接下来是Live Photo

Live Photo

imageManager.requestLivePhoto(for: asset, targetSize: pixelSize, contentMode: .aspectFill, options: options) {
(livePhoto, info) in // 使用示例项目中的 PHLivePhotoView
livePhotoView.livePhoto = livePhoto
}

Live Photos一直很受用户的关注,如何在应用中更好地呈现它们,非常重要,也非常简单。在如上的这个例子里,首先从图像管理器请求一张Live Photo,之后设置PHLivePhotoView。在你的应用里,用户可以通过轻触播放一张Live Photo,正如用户在内置“照片”应用里的操作一样。

Looping Video

swiftimageManager.requestPlayerItem(forVideo: asset, options: options) { 
playerItem, info in
DispatchQueue.main.async {
let player = AVQueuePlayer()
playerLooper = AVPlayerLooper(player: player, templateItem: playerItem)
playerLayer.player = player
player.play()
}}

在今年所推出的视频循环中,既包括弹跳效果,也包括Live Photo的循环效果。现在,在你的应用里播放这些和播放普通视频非常相似。可以请求播放器项目,并使用AVFoundation播放,还可以使用AVPlayerLooper取得循环效果。 可见,表现用户的媒体变得更为轻便,以他们真正想表现的方式,你也可以在自己的应用中,对这些全新的媒体类型,更为创新地表现。

iCloud 照片图库的改进

“iCloud 照片图库”可以与“照片”应用完美搭配使用。当用户开启“iCloud 照片图库”时,用户的照片和视频会被安全上传到 iCloud 中,同时这些更改会同步到用户的其他设备中。自动上传 iCloud 操作的触发条件是,设备连接到 Wi-Fi 且电量充足。根据用户的网络情况,在所有设备和 iCloud.com 上看到同步照片和视频所需的时间可能会不同。

使用 iPhone 拍照的用户,也常会使用“照片”相关的第三方应用。这些用户,大致可以分为 3 类:轻度用户、中度爱好者和重度专业用户。对于重度用户而言,由于自身图库中有很多内容,在第一次使用应用时,需要加载大批量的照片,这个过程中会十分耗时。而且这种耗时的加载状态,会对应用的用户体验大打折扣。

在 iOS 11 中,针对如何更快速高效地操作“大型照片图库”这一点做了优化,后面的内容会依次展开描述。

创建一个用于测试的“大型照片图库”

前面提到,如果图库中有大量内容,在应用加载数据时,会处于长时间的加载状态。而你如果想创建一个拥有大批量图片的图库,还是有些难度的。友好的是,Apple 为开发者提供了一个用户创建图库的示例应用:Photo Library Filler 。下载该应用并安装到测试设备,点击“Add Photos” 按钮,它便会迅速生成一个拥有大批量图片的图库供测试使用。

到这里,你就拥有了一个可用于测试的“大型照片图库”

现在,如何从“照片图库”提取图片

看下面这段代码:

let assets = PHAsset.fetchAssets(with: options)

这个方法用于从用户的“照片图库”提取图片,等号右侧用于提取资产,左侧为提取结果

let options = PHFetchOptions()options.predicate = NSPredicate(format: "isFavorite = %d", true)
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let assets = PHAsset.fetchAssets(with: options)

在上述代码中,首先提取库里所有的 Asset,并进行筛选,筛选条件为isFavorite = true,之后按照对应的创建日期进行排序。这时,如果在这些自定义提取里发现耗时,那么简化这里的筛选条件会十分必要。不同的筛选方式,可能会意味着查询耗时的巨大差异。造成这种差异的原因是你的操作可能在数据库优化路径之外进行,同时又试图回到优化路径中,这样就产生了不同的耗时差距。

比这种自定义提取更好的是,尽可能避免这种操作。例如下面这个例子中,我们实际上提取的是用户最喜欢智能相册。然后在智能相册里提取 Asset。这样既可以使用已有的关键字排序描述符,还可以保证操作是在数据库优化路径中进行的。

let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: nil)
let assets = PHAsset.fetchAssets(in: smartAlbums.firstObject, options: nil)

接着,看下返回结果。返回对象是一个PHFetchResult类型的对象。PHFetchResult 类型非常像一个数列,并且可以像数组一样来使用。但从内部实现来看,PHFetchResult和数列的工作机制还是完全不同的。并且这也是PhotoKit在大型图库操作方面,能够如此快速高效的原因之一。

我们来看看它的内部工作机制。

最开始,它只包含一个标识符列表。这意味着可以迅速返回对应的 Asset。但开始使用后,有更多工作必须执行。我们以一个枚举作为例子。

在这里,我们从索引 0 开始枚举。目前只有一个标识符,你还需要从数据库里提取元数据。为此创建一个PHAsset 对象,以便将 Asset 的元数据返回给你。

实际上,同时也创建了一个批处理

当我们继续枚举时,索引 1 和 2实际已经在内存中了。枚举继续,它将访问硬盘,获取后续 Asset 的元数据。

在提取结果量级较小的情况下,这样的提取,并不会有太大的影响。但如果提取结果包含10万个 Asset。其中,每一批都需要占用几 kb 内存。如果是10w 批,那将会产生几百兆的内存用量。更糟糕的是,每一批都需要几毫秒的提取时间,如果有10w 批,就需要消耗 10s来枚举这样的一个大型提取结果。所以,应该尽量避免枚举操作

在 PHFetchResult 中查询 Asset 更优的方式

实际开发过程中,枚举操作总是可能会出现的。这里列举一个例子。现在,你需要从一个提取结果中查询一个Asset 的索引

第一种方式,可以通过枚举该提取结果,通过“等于”比较返回的对象,来获取对应Asset 的索引。但是,枚举会非常耗时,所以更好的方法是通过另一种方式,使用高端 API

如上,通过使用indexOfObject来进行 Asset 索引的查询。而indexOfObject方法内部是通过比较“对象标识符”,以找到符合条件的 Asset,这样就不会有附加的“硬盘访问”和“数据库提取”。进而避免了第一种方式中,因为枚举出现的耗时操作。同样的,对containtsObject也是如此。

照片项目的拓展

一直以来,Apple 允许用户围绕照片创建丰富的有创意的项目。

PHProjectExtensionController 的引入

现在,“照片”中添加了一个新的扩展。对应的,Xcode中也添加了一个新模板,开发者在自己的应用里可以轻松创建这些扩展。此外,“照片”应用会自动发现你的拓展,大大提高了扩展应用被用户知道的概率。不仅如此,Apple 为了让这些扩展更容易为用户所发现,给这些扩展应用提供了App Store的直接链接。该链接会打开App Store窗口,并自动显示支持该扩展的应用。


扩展只存在于开发者的应用内。对于开发者来说,如果你在 App Store 已经有了一个关于 Photos 相关的应用。此时,就可以将扩展代码移动到该扩展空间内,并加以利用。之后,添加一个视图控制器,并实现PHProjectExtensionController协议。一切就位,“照片应用”便可以发现你的扩展了。

在用户选择“扩展应用”,并用它创建一个项目时,"照片应用"会发送一些字节数据(PHProjectExtensionContextPHProjectInfo)到对应的“扩展应用”。之后“照片应用”得到相应的返回结果,知道可以安装你的视图控制器。

过程中遵循的协议本身,对支持的项目类型有一个可选属性,可用于快速描述想让用户选择的选项。

optional public var supportedProjectTypes: [PHProjectTypeDescription] { get }

在实际使用过程中,用户既可以选择退出,也可以选择直接进入扩展。这些,在视图控制器里也有特定的函数方法。

public protocol PHProjectExtensionController : NSObjectProtocol {
//第一次使用该扩展创建项目时调用
public func beginProject(with extensionContext: PHProjectExtensionContext,
projectInfo: PHProjectInfo, completion: @escaping (Error?) -> Void)

//每次用户回到以前创建的项目时调用
public func resumeProject(with extensionContext: PHProjectExtensionContext,
completion: @escaping (Error?) -> Void)

//用户离开项目时调用
public func finishProject(completionHandler completion: @escaping () -> Void)
}

通过第一个函数方法,我们可以得到上下文及项目详细。在第一次使用该扩展创建项目时,会调用该方法。

第二个函数方法,我们同样可以获得上下文。在每次用户回到扩展项目时,会调用该方法。

最后一个函数方法。如果用户在扩展项目内,当他们决定切换离开,会调用该函数。通过回调,你可以清理任何正在处理的数据,或是关闭任何让处理器忙碌的任务,或是正在执行的动画。

PHProjectExtensionContext 是什么


PHProjectExtensionContext这个容器里,包含两个非常重要的对象。一个是PHProject,另一个是PHPhotoLibrary

PHProject 介绍

// PHProject.h 
// Photos
class PHProject : PHAssetCollection { var projectExtensionData: Data { get }
}

PHProject本身只是PHAsset的一个子集。在子集中,创建PHProject,只添加了一个非常重要的属性projectExtensionData。可以用于保存任何你需要的数据,它是你正在使用的资产标识符的列表。也许是一些基本的布局信息配置信息。同时属性projectExtensionData并不是为了照片缓存、缩略图之类的存在。因为这些功能,你可以快速地创建或把它们缓存到其他位置。为了它小而有用,抛开了这些功能,并且projectExtensionData被限制定为1 兆。因为这里面的信息,是一系列的字符串,所以1 兆大小已经足够了。即使用户不断创建项目,也不会增大用户的库。

PHProjectChangeRequest

do {
let changeRequest = PHProjectChangeRequest(project: self.project)
try self.library.performChangesAndWait {
changeRequest.projectExtensionData = NSKeyedArchiver.archivedData(withRootObject: cloudIdentifiers)
}} catch {
print("Failed to save project data: \(error.localizedDescription)")
}

设置数据非常简单。只需按上述实例化,实例化后,可以在Photo Library调用performChangesAndWait函数,在里面将数据设置成任何你想要的样式。

PHProjectInfo 介绍

最高层的ProductInfo分为下面这几个区:

当看到这个构造时,可能会问,为什么数组里面还有数组。为什么是这种嵌套结构。但是如果想想“照片”应用里的“回忆”功能,会发现这些是有道理的。

“回忆”本身建立于大量资产之上,允许用户回忆时,可以切换“显示照片摘要”“显示所有照片”。通过下面的一张图解,可以更清晰地描述为什么使用数组。

Section Contents数组是已排序数组。索引为 0的对象是最优内容,是资产集合最精炼的摘要;而数组末端的对象内容是最多的,包含了所有的照片数据。开发过程中,开发者需要根据具体的需求,选择性地使用。

PHCloudIdentifier 介绍

PHCloudIdentifier是一个全新的概念。当你想把数据存到用户的Photo Library时,数据可能会被同步到用户其他的设备中。为了确保在保存的数据,合理地同步到其他设备中,iOS 11 推出了一个新对象PHCloudIdentifier

//获取当前 Cloud Identifiers
cloudIdentifiers += dataDict.value(forKey: "contentIdentifiers") as! [PHCloudIdentifier]

//转换为 Local Identifiers
let localIdentifiers = self.library.localIdentifiers(for: cloudIdentifiers)

可以将它看做资产的全局标识符,但也并不像全局字符串那么简单,因为还需要处理同步和同步状态等情况。而这些复杂操作,系统已经为我们做了。你必须要做的唯一操作是,在提取之前,确保你的转换总是从全局标识符本地标识符。可以通过PHPhotoLibrary里的方法来进行双向转换。

关于“视图布局”的改进

例如上述这种网格布局,对用户来说是很愉快的。如果开发者可以直接访问这个布局,不是会很棒吗?在 iOS 11 中,你确实可以进行访问了。

为了支持访问,系统首先确定了一个坐标系。

如果你查看“回忆”功能,会发现所有内容都被排列在一个由 4x3 单元格构成的网络中。但它是一个非正方形尺寸,不利于拓展。所以又有了下面这样的结构。

例如上述实例图片的的布局,可以被转化为一个由 20 个统一列组成的网络空间。确定了这个坐标系,就可以根据需求任意缩放。

并且,通过这个坐标系,也可以和系统互相传递坐标信息。例如上图的坐标为(0, 0, 8 , 9)

PHProjectElement 介绍

// PHProjectElementclass 
PHProjectElement : NSObject, NSSecureCoding {
//权重的范围是 0.0 - 1.0,默认为 0.5
var weight: Double { get }
//元素在网络布局中的坐标
var placement: CGRect { get }
}

Section Content里,系统提供了一组元素。所有元素都是PHProjectElement的子集。这里有两个非常重要的属性:

  • placement(位置)
  • weight(权重)

“位置”属性,前面已经有所介绍了,这里介绍下“权重”属性。再次回到“回忆”功能,在大量资产中如果想确定最相关的照片,系统需要有自己的评分系统。这里,评分系统通过给每个元素一个权重值,来代表每个元素的重要性。权重值从0.01.0,默认值是0.5,也就是说,权重值为0.5的资产代表普通。

RegionsOfInterest 是什么

var regionsOfInterest: [PHProjectRegionOfInterest] { get }

这里,有这样的一个概念,称为“兴趣区”。也就是这里的regionsOfInterest属性,这是PHProjectAssetElement所特有的。

macOS API里,已经有很多方法,可以用来进行面部识别,从而寻找图片中的脸。但从这些方法无法知道这些脸的相关性。来看下面一副示例图:

你会注意到,这些脸部有对应的标识符。在不同的照片里相同的脸,会看到被标记为相同的标识符。这样的表示十分有趣。如果你正在处理动画、幻灯片等效果,这将非常有用。因为你现在可以真正把大集合中的图片位置彼此联系起来了。对于体验上的改进来说,这会是一个很棒的属性。

总结

本节主要介绍了 Photos APIs 的新特性,主要包含了以下几点内容:

  • 改进的授权模式
  • 大幅优化的 UIImagePickerController
  • 全新的图片格式 HEIF
  • 大型图片库的创建
  • 及为 Photos 创建项目扩展

回头来看开篇提出的三点疑问:

  • 如何以一种不违反用户信任的方式获取及保存内容到相册?
  • 是否可以为 Photo Library 创建扩展内容?
  • 如何在应用中简单、高效地实现这些操作?

这些,在新的 Photos APIs 里都有了相应的解决方案。综上可以看到 Photos 也在越来越完善,可扩展性越来越强,功能也在越来越强大。

相关资料

评论