Photos框架及坑(转自简书)

导读

对于 Photos 框架的介绍,推荐观看 objccn.io 的文章。写得真好,我写得的文章水准还差得老远啊。本文总结了近期使用 Photos 框架编写一个相册的经验,目前还有很大一部分的框架内容没有涉及到,后续会更新内容。html

获取资源

照片库中有两种资源可供获取:PHAssetPHCollection,前者表明图像或视频对象,后者是前者的集合或自身类型的集合。PHCollection是个基类,有PHAssetCollectionPHCollectionList两个子类,分别表明 Photos 里的相册和文件夹。以往使用 Photos 时,并无注意到能够创建文件夹,彷佛是从 Photos 框架才支持这个功能,而PHCollectionList里可嵌套PHAssetCollection和自身类型,还支持多重嵌套。获取PHAsset以及PHAssetCollection的过程相似于 Core Data,以下所示,只能经过类方法来返回PHFetchResult,遍历返回的结果来获取须要的资源。ios

PHAsset Fetch Methodswift

PHAssetCollection Fetch Method缓存


注意,PHAssetPHAssetCollectionPHCollectionList 都是轻量级的不可变对象,使用这些类时并无将其表明的图像或视频或是集合载入内存中,要使用其表明的图像或视频,须要经过PHImageManager类来请求。app

请求图像(这里有巨坑)

关于PHImageManager类,NSHipster 有篇总结文章不错。框架

- requestImageForAsset:targetSize:contentMode:options:resultHandler:

你不该该生成该类的实例,而应该使用该类的提供的单例对象。该方法提供指定的尺寸的图像,与ALAssetsLibrary库相比,没有了方便的缩略图提供。不过要吐槽的是,ALAssetsLibrary库提供的缩略图每每尺寸过小而且质量很低,用在 TableView 上还能够。异步

须要注意的是,该方法在默认状况下是异步执行的,并且 Photos 库可能会屡次执行 resultHandler 块,由于对于指定的尺寸,Photos 可能会先提供低质量的图像以供临时显示,随后会将指定尺寸的图像返回。若是指定尺寸的高质量的图像有缓存,那么直接提供高质量的图像。而这些行为,能够经过 options 参数来定制。ide

PHImageRequestOptions类用于定制请求。这里有巨坑。上面的方法返回指定尺寸的图像,若是你仅仅指定必要的参数而没有对 options 进行配置的话,返回的图像尺寸将会是原始图像的尺寸。或者,你指定的尺寸很小,这时候会按照你的要求来返回接近该尺寸的图像。在个人 iPad mini 一代上,对于自拍的图像,指定尺寸不超过(257, 257)的话,返回的图像尺寸和你预期的同样,其余状况下都是原始尺寸。PHImageRequestOptions有如下几个重要的属性:fetch

synchronous:指定请求是否同步执行。
resizeMode:对请求的图像怎样缩放。有三种选择:None,不缩放;Fast,尽快地提供接近或稍微大于要求的尺寸;Exact,精准提供要求的尺寸。
deliveryMode:图像质量。有三种值:Opportunistic,在速度与质量中均衡;HighQualityFormat,无论花费多长时间,提供高质量图像;FastFormat,以最快速度提供好的质量。
             这个属性只有在 synchronous 为 true 时有效。
normalizedCropRect:用于对原始尺寸的图像进行裁剪,基于比例坐标。只在 resizeMode 为 Exact 时有效。

resizeMode 默认是 None,这也形成了返回图像尺寸与要求尺寸不符。这点须要注意。要返回一个指定尺寸的图像须要避免两层陷阱:必定要指定 options 参数,resizeMode 不能为 None。ui

除了必有的请求图像或是视频的功能外,PHImageManager添加了两大功能:
1.缓存图像,由其子类PHCachingImageManager实现,缓存效率和空间管理能知足大部分场景的需求;
2.裁剪图像,这个功能好久之前就有强烈的需求。六年前 StackOverflow 上 Cropping a UIImage 这个问题就被提出来了,方法也五花八门,然而这些方法可能会有各类小问题。官方的方法能让你避免这些小问题。使用方法能够参考 NSHipster 的总结文章里用人脸识别获取头像的例子。

localIdentifier vs URL

Photos 框架推出时,和原来的照片库 AssetsLibrary 框架之间还有些交互,PHAsset 类的+ fetchAssetsWithALAssetURLs:options:PHAssetCollection类的 + fetchAssetCollectionsWithALAssetGroupURLs:options:能够利用原来的 AssetsLibrary 提供的 URL 进行转化,而在 iOS 9 中,原来的照片框架 AssetsLibrary 已经被废弃了,现在这两个方法也没有用处了。当初我还找过如何从 Photos 框架到 AssetsLibrary 框架的方法,理所固然地白费功夫,官方要淡化照片库中 URL 的概念,改之使用一个标志符来惟一表明一个资源。Photos 框架中的根类PHObject只有一个公开接口localIdentifier,AssetsLibrary 框架中不管是 Asset 仍是 AssetGroup 的 URL 也是惟一标志符,并且同时仍是动态变化的,每次启动应用后获取的 URL 和上一次是不同的,而 AssetGroup 有一个 PersistentID 与PHObjectlocalIdentifier相似,但获取比较麻烦。
localIdentifier属性带来的最大好处是PHObject类实现了 NSCopying 协议,能够直接使用localIdentifier属性对PHObject及其子类对象进行对比是否同一个对象。

获取指定类型相册

这是最基本的一个用途,可是每次隔了几天就忘了具体的类型。
经过PHAssetCollection的如下方法来获取指定的相册:

func fetchAssetCollectionsWithType(_ type: PHAssetCollectionType, subtype subtype: PHAssetCollectionSubtype, options options: PHFetchOptions?) -> PHFetchResult

这个方法须要至少指定两个参数:

enum PHAssetCollectionType : Int {
    case Album //从 iTunes 同步来的相册,以及用户在 Photos 中本身创建的相册
    case SmartAlbum //经由相机得来的相册
    case Moment //Photos 为咱们自动生成的时间分组的相册
}

enum PHAssetCollectionSubtype : Int {
    case AlbumRegular //用户在 Photos 中建立的相册,也就是我所谓的逻辑相册
    case AlbumSyncedEvent //使用 iTunes 从 Photos 照片库或者 iPhoto 照片库同步过来的事件。然而,在iTunes 12 以及iOS 9.0 beta4上,选用该类型无法获取同步的事件相册,而必须使用AlbumSyncedAlbum。
    case AlbumSyncedFaces //使用 iTunes 从 Photos 照片库或者 iPhoto 照片库同步的人物相册。
    case AlbumSyncedAlbum //作了 AlbumSyncedEvent 应该作的事
    case AlbumImported //从相机或是外部存储导入的相册,彻底没有这方面的使用经验,无法验证。
    case AlbumMyPhotoStream //用户的 iCloud 照片流
    case AlbumCloudShared //用户使用 iCloud 共享的相册
    case SmartAlbumGeneric //文档解释为非特殊类型的相册,主要包括从 iPhoto 同步过来的相册。因为本人的 iPhoto 已被 Photos 替代,没法验证。不过,在个人 iPad mini 上是没法获取的,而下面类型的相册,尽管没有包含照片或视频,但可以获取到。
    case SmartAlbumPanoramas //相机拍摄的全景照片
    case SmartAlbumVideos //相机拍摄的视频
    case SmartAlbumFavorites //收藏文件夹
    case SmartAlbumTimelapses //延时视频文件夹,同时也会出如今视频文件夹中
    case SmartAlbumAllHidden //包含隐藏照片或视频的文件夹
    case SmartAlbumRecentlyAdded //相机近期拍摄的照片或视频
    case SmartAlbumBursts //连拍模式拍摄的照片,在 iPad mini 上按住快门不放就能够了,可是照片依然没有存放在这个文件夹下,而是在相机相册里。
    case SmartAlbumSlomoVideos //Slomo 是 slow motion 的缩写,高速摄影慢动做解析,在该模式下,iOS 设备以120帧拍摄。不过个人 iPad mini 不支持,无法验证。
    case SmartAlbumUserLibrary //这个命名最神奇了,就是相机相册,全部相机拍摄的照片或视频都会出如今该相册中,并且使用其余应用保存的照片也会出如今这里。
    case Any //包含全部类型
}

有些参数的命名十分使人困惑,我每次看了都晕菜。新的 Photos Kit 框架是在 iOS 8 中推出的,主类型分为三种类型:Album,SmartAlbum 以及 Moment。然而,对于前二者的分类我是比较困惑的。Mac 上支持智能文件夹,就是能够无论文件的物理位置而将一系列文件集合起来创建一个文件夹,能够说是物理相册和逻辑相册。而在 iOS 上,SmartAlbum 却给了相机衍生的相册,用户收集不一样照片创建的逻辑相册被归类到 Album 类型的 AlbumRegular 下,十分反个人直觉。在文档中,SmartAlbum 是指内容会动态变化的相册,这样一来又有一个比较困惑的设计,PHAssetCollection 类有个属性 estimatedAssetCount,能够用来快速获取该相册中的照片和视频的数量,可是在 SmartAlbum 上该属性永远为0,动态相册没能实现对数量的监测。

注意,获取指定类型的相册时,主类型和子类型要匹配,不要串台。若是不匹配,系统会按照 Any 子类型来处理。对于 Moment 类型,子类型使用 Any。
1.获取用户本身创建的相册和文件夹(我称之为逻辑相册,非系统相册和从 iTunes 同步来的相册)有两种方法:

PHCollection.fetchTopLevelUserCollectionsWithOptions(nil) 
PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .AlbumRegular, options: nil)

在没有提供PHOptions的状况下,返回的PHFetchResult结果是按相册的创建时间排序的,最新的在前面。
2.获取相机相册:

PHAssetCollection.fetchAssetCollectionsWithType(.SmartAlbum, subtype: .SmartAlbumUserLibrary, options: nil)

另外PHAsset的获取方式在 iOS 8.1 后发生了一些变化。如下的两个方法在 iOS 8.1后再也不包含从 iTunes 同步以及在 iCloud 中的照片和视频。要获取 iOS 设备上本地的全部照片和资源只能从 PHAssetCollection 入手了。

+ fetchAssetsWithMediaType:options:
 + fetchAssetsWithOptions:

添加、删除、编辑

对照片库进行操做,可参见官方文档 Requesting Changes to the Photo Library,照片库中的资源都有对应的变动请求类:PHAssetChangeRequest, PHAssetCollectionChangeRequestPHCollectionListChangeRequest, 而这些操做的请求都要求在PHPhotoLibraryperformChanges(_ changeBlock: dispatch_block_t!, completionHandler completionHandler: ((Bool, NSError!) -> Void)!)中的 changeBlock 中执行。注意,这里只是发出请求并无作出实质的更改,所以想要根据更改结果更新 UI 的话不要在 completionHandler 中进行,而应该在 photoLibraryDidChange(changeInfo: PHChange!)中进行。三种变动请求中,删除和编辑操做都比较简单,而添加操做有须要注意的地方。

添加操做: placeholder 的用处

在相册中添加照片:

let createAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
let assetPlaceholder = createAssetRequest.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: album)
albumChangeRequest.addAssets([assetPlaceholder])

在文件夹中添加相册:

let fetchResult = PHCollection.fetchCollectionsInCollectionList(collectionList, options: nil)
let createSubAlbumRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(title!)
let albumPlaceholder = createSubAlbumRequest.placeholderForCreatedAssetCollection
let folderChangeRequest = PHCollectionListChangeRequest.init(forCollectionList: collectionList, childCollections: fetchResult)
folderChangeRequest?.addChildCollections([albumPlaceholder])

在文件夹中添加子文件夹:

let fetchResult = PHCollection.fetchCollectionsInCollectionList(collectionList, options: nil)
let createSubFolderRequest = PHCollectionListChangeRequest.creationRequestForCollectionListWithTitle(title!)
let subfolderPlaceholder = createSubFolderRequest.placeholderForCreatedCollectionList
let folderChangeRequest = PHCollectionListChangeRequest.init(forCollectionList: collectionList, childCollections: fetchResult)
folderChangeRequest?.addChildCollections([subfolderPlaceholder])

处理变动

对相册发出变动请求后,系统会通知用户是否容许,用户容许后才会发生实质上的变化,系统会发布通知。
首先,注册成为PHPhotoLibrary的观察者来接收变化通知:

PHPhotoLibrary.shareLibrary().registerChangeObserver(self)

而后,实现PHPhotoLibraryChangeObserver协议的photoLibraryDidChange(changeInfo: PHChange!)。官方有个很好的例子:Handling Changes: An Example,有如下几点须要注意:
1.在photoLibraryDidChange(changeInfo: PHChange!)的实现里将全部处理放在主线程里处理;
2.全部PHPhotoLibrary的观察者都会收到通知,无论观察者自己引用的内容是否发生变化,所以要根据观察者的状况来对通知进行过滤。从参数PHChange对象里能得到全部的变化,经过changeDetailsForObject:changeDetailsForFetchResult:来获取细节。changeDetailsForObject:获取的细节只是PHObject子类对象自己的信息变化,包括是否有成员被删除以及是否有图像或视频发生变化两种信息,有用信息实在有限,要处理成员变化须要依靠后者;对一个PHFetchResult对象使用changeDetailsForFetchResult:获取的细节中只包含该PHFetchResult对象变化的信息,能够利用这点来对通知进行过滤处理。
3.经过changeDetailsForFetchResult:获取的PHFetchResultChangeDetails对象,包含了 FetchResult 的结果的全部变化状况以及 FetchResult 的成员变化先后的数据,须要注意的是成员变化的通知。
例如,经过

var rootCollectionsFetchResult = PHCollection.fetchTopLevelUserCollectionsWithOptions(nil)

获取全部用户创建的相册和文件夹,在photoLibraryDidChange(changeInfo: PHChange!)中经过如下方法得到PHFetchResultChangeDetails对象。

let fetchChangeDetails = changeInstance.changeDetailsForFetchResult(rootCollectionsFetchResult)

fetchChangeDetails.changedObject返回一组其内容或元数据发生变化的成员,返回的成员是更新后的成员对象。当用户对某个文件夹内的相册或子文件夹进行添加、删除和编辑操做即文件夹的内容而不是文件夹自己的属性发生变化时,通知中会该变化的信息吗?实际上只有在文件夹中添加相册或子文件夹时才会在fetchChangeDetails.changedObject中有所反应,而删除成员或是修改元数据等操做都不会在通知有所反应,你须要使用其余手段来跟踪变化。


 

文/seedante(简书做者) 原文连接:http://www.jianshu.com/p/42e5d2f75452/ 著做权归做者全部,转载请联系做者得到受权,并标注“简书做者”。

相关文章
相关标签/搜索