Photos框架之Live Photo相关

前言

终于来到这个系列的最后一篇了 —— Live Photo

Live Photo 是伴随着 iPhone 6s、iOS 9 和 3D-Touch 一起问世的,就是一种用力按可以动起来的照片。说是照片,其实是一张照片+一段3秒左右的视频。拍摄照片的时候如果打开了 Live Photo 的选项,其实手机一直在录视频,当你按下快门的时候,手机就会帮我们拍摄下按快门时瞬间的照片,再保存按快门前后1.5秒的视频。这样“一张” Live Photo 就形成了。使用 3D-Touch 重按某张 Live Photo 的时候,它就会自动播放那段视频,看起来好像照片不是静态的一样。我个人觉得 Live Photo 还是蛮有意思的,主要是可以记录下拍摄照片前后的动作和声音,一段时间之后翻看以前拍摄的照片,如果只是静态的,你可能可以回忆起当时发生了什么,但如果照片还是可以动的,那你就会感觉好像真实地回到了拍摄的那个时候一样。

废话不多说,接下来就来说一下在开发中怎么与 Live Photo 打交道。

从系统图库获取 Live Photo

还是和之前一样,首先我们假设已经拍了一些 Live Photo 照片存在了系统图库中,所以第一件要做的事情,就是从系统图库中拿到这些 Live Photo。

Live Photo 和普通的图片不一样,所以肯定不能用 UIImage 对象来表示了,Apple 给我们提供了一个专门 —— PHLivePhoto 的对象用来标识 Live Photo。要使用这个 PHLivePhoto 对象,另外也需要一个 view 容器来展示,即 PHLivePhotoView

和获取普通照片一样,Photos 框架给我们提供了方法从系统图库中获取 Live Photo 的方法。我就直接贴上我简单封装之后的代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
从系统图库请求 Live Photo 对象

- parameter asset: 代表 Live Photo 的对象
- parameter completion: 完成后的回调,返回获取到的 Live Photo 对象
*/
static func requestLivePhoto(for asset: PHAsset, completion: @escaping (PHLivePhoto?, [AnyHashable : Any]?) -> Swift.Void) {

let option = PHLivePhotoRequestOptions()

option.version = .current
option.deliveryMode = .highQualityFormat
option.isNetworkAccessAllowed = true

PHCachingImageManager.default().requestLivePhoto(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: option) { (livePhoto, info) in

if let info = info {
let isDegraded = info[PHImageResultIsDegradedKey] as? Bool
if isDegraded != true {
// 执行回调,将获得到的图片传出
completion(livePhoto, info)
}
} else {
completion(nil, nil)
}

}
}

可以看到,我们在 完成回调 中得到的 Live Photo 对象确实是 PHLivePhoto 类型的。如果只是为了显示,那么只要在创建一个 PHLivePhotoView 对象,将 PHLivePhoto 对象添加到它上面即可。就好比在 UIImageView 上面添加 UIImage 对象。PHLivePhotoView 对象上面默认已经带有了 3D-Touch 的手势,所以直接就可以用。

但是如果我们要将 Live Photo 保存到沙盒中去,那就不是这么做了。

保存到沙盒

保存 Live Photo 到沙盒与保存普通照片或者视频到沙盒(之前的博文中讲过)很类似,最后也是调用如下这个封装方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
将 PHAsset 对应的资源数据写入沙盒

- parameter resource: 图像资源
- parameter path: 存储路径
- parameter completion: 完成后的回调
*/
static func saveData(for resource: PHAssetResource, toPath path: String, completion: @escaping (Error?) -> Swift.Void) {

let option = PHAssetResourceRequestOptions()

option.isNetworkAccessAllowed = true

let url = URL(fileURLWithPath: path)

let resourceManager = PHAssetResourceManager.default()

resourceManager.writeData(for: resource, toFile: url, options: option, completionHandler: { (error) in
// 执行回调
completion(error)
})

}

这个方法传入的是 PHAssetResource,这个其实我们之前都讲过。Live Photo 和普通照片的不同在哪里呢?就在于 Live Photo 是由一张照片和一段视频组成的,所以一个 Live Photo 其实对应着两个 PHAssetResource 对象,而普通的照片或者视频就只有一个。当然,这里也要说明一下,一个或者两个说的是普通情况,当你在系统图库中对照片或者 Live Photo 进行过编辑的话,其实 PHAssetResource 对象也就不止一个两个了。但现在先不考虑这种情况。

当然不管最后可以得到几个 PHAssetResource 对象,实际上,一张照片或者视频或者 Live Photo 都一一对应一个 PHAsset 对象,你最先从系统图库中得到的也是PHAsset 对象,你用它来获取PHLivePhoto对象(上一节说的), 也通过它来得到 PHAssetResource 对象(现在正在说的)。获取PHAssetResource对象主要通过PHAssetResource类的下面这个方法:

1
open class func assetResources(for asset: PHAsset) -> [PHAssetResource]

可以看到返回值是个数组,所以它是根据 PHAsset 究竟对应普通照片视频、Live Photo或者编辑过的照片等有所不同的。

从沙盒获取 Live Photo

假设 Live Photo 已经成功保存到了沙盒中,现在我要将它取出来展示,那该怎么做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
从沙盒获取 Live Photo 对象

- parameter paths: Live Photo 对应的照片和视频路径
- parameter image: 占位图
- parameter completion: 完成后的回调,返回获取到的 Live Photo 对象
*/
static func requestLivePhoto(with paths: [String], placeholderImage image: UIImage?, completion: @escaping (PHLivePhoto?, [AnyHashable : Any]) -> Swift.Void) {

let photoURL = URL(fileURLWithPath: paths[0])
let videoURL = URL(fileURLWithPath: paths[1])

PHLivePhoto.request(withResourceFileURLs: [photoURL, videoURL], placeholderImage: image, targetSize: CGSize.zero, contentMode: .default) { (livePhoto, info) in

let isDegraded = info[PHImageResultIsDegradedKey] as? Bool
if isDegraded != true {
// 执行回调,将获得到的图片传出
completion(livePhoto, info)
}

}

}

从上面这个封装方法可知,我们首先需要获取 Live Photo 对应的照片和视频的存储路径 URL,然后调用 PHLivePhoto 类的 request 方法从沙盒中将其取出来。取出来的 Live Photo 同样是 PHLivePhoto 对象。

保存到系统图库

按照惯例,最后还要说一下如果将 Live Photo 反过来保存到系统图库中。关于这个,我也封装了一个方法,直接调用如下方法保存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
保存 Live Photo 到系统图库

- parameter path: 要保存的 Live Photo 的资源(照片和视频)的路径
- parameter completion: 完成回调
*/
static func saveLivePhotoToPhotoLibrary(livePhotoResourcePaths paths: [String], completion: ((Bool, Error?) -> Swift.Void)?) {

let status = PHPhotoLibrary.authorizationStatus()

// 判断用户授权状态
guard status != .denied || status != .restricted else {

// 执行完成回调
if completion != nil {
completion!(false, nil)
}

return
}

PHPhotoLibrary.shared().performChanges({

let photoURL = URL(fileURLWithPath: paths[0])
let videoURL = URL(fileURLWithPath: paths[1])

let request = PHAssetCreationRequest.forAsset()

request.addResource(with: .photo, fileURL: photoURL, options: nil)
request.addResource(with: .pairedVideo, fileURL: videoURL, options: nil)

}, completionHandler: { (success, error) in

// 执行完成回调
if completion != nil {
completion!(success, error)
}

})

}

简单讲解一下,最开始的权限检查自不多说,主要是要看 PHPhotoLibrary.shared().performChanges() 这个方法,它接收一个闭包作为参数,我们主要在这个闭包里面执行保存的操作。

因为 Live Photo 现在存储在沙盒中,我们自然要先定位它,所以要先得到对应的照片和视频的路径 URL。接下来创建一个 PHAssetCreationRequest 对象,将 Live Photo 对应的照片和视频作为 resource 添加到 PHAssetCreationRequest 对象上。addResource方法声明如下:

1
open func addResource(with type: PHAssetResourceType, fileURL: URL, options: PHAssetResourceCreationOptions?)

它的第一个参数就是一个 PHAssetResourceType ,这个类型有很多,比如 photovideo 之类的。但是 Live Photo 的一对照片和视频的这个这个参数比较特殊,可以看到它的视频的类型参数为 pairedVideo。只有传入了这个参数,系统才会知道,这个视频不是单独的一个视频,而是和某张照片配对组成 Live Photo 的视频。如果将这个类型写成普通的 video,那么最后保存在系统图库中的将会是一张照片和一段视频,不再是一张 Live Photo。
那么是不是随便找一张照片和一段视频就可以配对呢?当然不行!能够配对的照片和视频,系统其实是在它们上面做上了相同的标记的。

我们之前讲解保存照片到系统图库的时候,说到了利用 UIActivityViewController 来保存。这里 Live Photo 是否也可以呢?

先根据之前说的方法从沙盒中获取到 PHLivePhoto 对象,然后将其传给 UIActivityViewController 的 activityItems

如上图所示,UIActivityViewController 确实识别出了 Live Photo,并出现了保存照片的选项,但是,点击“存储 Live Photo 照片”,并没有成功将 Live Photo 保存到系统图库中,也就是仅仅这样做,并不能将 Live Photo 成功保存。

于是后来我想了一个办法,UIActivityViewController 有一个属性叫做 completionWithItemsHandler,它接收一个闭包,用于在操作执行完毕之后调用。所以我就想既然直接点击保存没有用,那我就手动保存吧,我在 completionWithItemsHandler 中写代码保存 Live Photo。但是因为这个 completionWithItemsHandler 在任何 UIActivityViewController 的操作执行完成之后都会调用,如果我并不是想保存照片,而是点了分享,那就不应该保存,所以还需要加上判断,只有确实点击了“存储 Live Photo 照片”,才执行保存的操作。最后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
PhotoTool.requestLivePhoto(with: livePhotoResourcePaths!, placeholderImage: nil, completion: { (livePhoto, info) in

guard livePhoto != nil else { return }

let activityController = UIActivityViewController(activityItems: [livePhoto!], applicationActivities: nil)

activityController.excludedActivityTypes = [UIActivityType.postToFlickr, UIActivityType.postToVimeo, UIActivityType.postToTencentWeibo]

// 因为系统的保存对 Live Photo 没有效果,所以加上这个回调,手动保存
activityController.completionWithItemsHandler = { (type, success, _, _) in

// 只有点击保存到相册后才执行下面的操作
guard type == .saveToCameraRoll && success else { return }

// 手动保存到相册
PhotoTool.saveLivePhotoToPhotoLibrary(livePhotoResourcePaths: livePhotoResourcePaths!, completion: nil)

}

// 解决 iPad 弹出 UIActivityViewController 奔溃的问题
activityController.popoverPresentationController?.barButtonItem = sender as? UIBarButtonItem

// 弹出视图
self.present(activityController, animated: true, completion: nil)

})

PhotoTool 中封装了我们之前说的 requestLivePhoto 方法,将 UIActivityViewController 的方法写在其完成回调中。

总结

今天简单介绍了一些 Live Photo 相关的操作,包括常规的从系统图库获取 Live Photo、保存 Live Photo 到沙盒、从沙盒获取 Live Photo 以及保存 Live Photo 到系统图库。

到目前为止,整个 Photos 框架我想要讲的东西都讲完了,这个系列终于可以告一个段落了。Photos 框架当然还有很多很多东西,等着我们去学习。