一、介绍
PhotoKit
是App在使用、管理图片和视频的框架,而且还包括了iCloud上面的图片以及及时照片。(iOS8+)
二、概要
在iOS中,PhotoKit
支持应用构建照片以及编辑扩展,还可以直接访问管理照片和视频元资源以及元资源集合例如专辑、时刻和共享时刻。
smartAlbums 获取智能相册(例如系统创建的最近项目、截屏、全景照片等)
fetchAssetCollections(用户自己创建的相册)
大致流程:权限获取->相册集拉取->照片集拉取->照片缩略图下载展示->原图、原视频下载->增删改->监听刷新
PhotoKit 对象模型
PhotoKit 定义了与系统的 Photos 应用内展现给用户的模型对象相一致的实体图表。这些照片实体都是轻量级的不可变对象。所有的 PhotoKit 对象都是继承自 PHObject
抽象基类,其公共接口只提供了一个 localIdentifier 属性。
PHAsset
表示用户照片库中一个单独的资源,用以提供资源的元数据。
PHCollection
两个子类 PHCollectionList
和PHAssetCollection
成组的资源叫做资源集合,用 PHAssetCollection
类表示。一个单独的资源集合可以是照片库中的一个相册或者一个时刻,或者是一个特殊的“智能相册”。这种智能相册包括所有的视频集合,最近添加的项目,用户收藏,所有连拍照片等等。
PHCollectionList
表示PHAssetCollection
集合`(可以理解为二位数组)。实际上,我们可以在照片应用的时刻栏目中看到它:照片 — 时刻 — 精选 — 年度,就是一个例子。
PHPhotoLibrary单例对象,用来维护照片库。
(1)对相册内容进行修改(添加图片、删除图片、新建相册等)(例如:PHAssetChangeRequest,PHAssetCollectionChangeRequest, PHCollectionListChangeRequest等)
(2)监听相册内容的变化(photoLibraryDidChange)
PHFetchResult获取结果
无论是获取相册,还是相册的图片视频资源,都是返回一个PHFetchResult
。访问获取的结果(按需从备份存储区中获取对象,而不是一次性全部获取,获取的对象将被保存在缓存中,并在内存有压力的情况下清除)。具体需要的资源
PHChange
负责变化监测:首先通过canPerfromEditOperation
方法检验collection或asset是否有变化, (2)如果有变化可以调用changeDetailsForObject
或者changeDetailsForFetchResult
方法,它返回给我们一个PHObjectChangeDetails
对象,是对最新的照片实体对象的引用,可以告诉我们对象的图像数据是否曾变化过、对象是否曾被删除过。
获取指定类型相册
获取相册类型必须的2个参数
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 //包含所有类型
}
需要注意:获取指定类型的相册时,主类型和子类型要匹配,不要串台。如果不匹配,系统会按照 Any 子类型来处理。对于 Moment 类型,子类型使用 Any。
在没有提供PHOptions的情况下,返回的PHFetchResult结果是按相册的建立时间排序的,最新的在前面。
1、获取用户自己建立的相册和文件夹有俩种方法
PHCollection.fetchTopLevelUserCollectionsWithOptions(nil)
PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .AlbumRegular, options: nil)
2、获取相机相册:
let tmpOptions = PHFetchOptions()
// startDate, endDate, estimatedAssetCount
tmpOptions.sortDescriptors = [NSSortDescriptor(key: "startDate", ascending: isAscending)]
// PHAssetCollectionType和PHAssetCollectionSubtype
let collections = PHAssetCollection.fetchAssetCollections(with: type, subtype: subtype, options: options)
最后我们需要对获取的collections根据需求进行筛选(例如去除空相册或则PHCollectionList)
获取照片资源
// 对返回资源的配置,包括对资源的过滤、排序等
let options = PHFetchOptions()
// 获取指定类型资.image .viddeo (如果不设置默认图片和视频都获取)
options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue)
// 排序(creationDate, modificationDate, duration, pixelWidth, pixelHeight)
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: isAscending)]
// 传入PHAssetcollection和筛选配置
let fetchResult = PHAsset.fetchAssets(in: collection, options: options)
判断资源类型
- 通过
PHAsset
的PHAssetMediaType
判断是image还是video
public enum PHAssetMediaType : Int {
case unknown // 未知
case image // 图片
case video // 视频
case audio // 音频
}
- 判断是否是实时照片PHLivePhoto(iOS9.1+ 6s及以上支持 和
PHAsset
一样也是一个资源包,不同的是他不仅包含一张图片,而且还有一段mov格式的视频(拍摄该照片时前后几秒的视频))
if #available(iOS 9.1, *) {
if asset.mediaSubtypes.contains(.photoLive) {
// 实时照片(以后可以加重压播放视频 用户可以选择时使用照片还是使用视频)
}
}
- 判断是否是iCloud图
@objc open class func judgeAssetis(inLocalAblum asset: PHAsset?) -> Bool {
var result = false
let option = PHImageRequestOptions()
option.isNetworkAccessAllowed = false
option.isSynchronous = true
if let asset = asset {
PHCachingImageManager.default().requestImageData(for: asset, options: option, resultHandler: { imageData, _, _, _ in
result = imageData != nil ? true : false
})
}
return result
}
照片和视频资源下载
/**
asset: 资源元数据
targetSize: 返回图片的尺寸
contentMode :决定了照片应该以按比例缩放还是按比例填充的方式放到目标大小内。
requestOptions: 定制请求配置参数
*/
// 照片拉取:
PHImageManager.default().requestImageForAsset(for: asset,targetSize: thumbnailSize, contentMode: .aspectFill, options: requestOptions, resultHandler: resultHandler)
// 视频拉取
PHImageManager.default().requestPlayerItem(forVideo: asset, options: options, resultHandler: resultHandler)
// 实时照片
PHImageManager.default().requestLivePhoto(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { (livePhoto, info) in
})
拉取资源请求参数PHImageRequestOptions
1.1 isSynchronous。是否同步处理一个图像请求。默认为NO
/*
- 1、如果`synchronous `属性为YES,一定为同步请求。resultHandler只会执行一次,并且返回高质量的图片。
- 2、如果`synchronous `属性为NO,不一定时异步。resultHandler是否会被多次调用取决于deliveryMode属性:
- .HighQualityFormat ,resultHandler调用一次,框架只返回高质量图。
- .FastFormat,resultHandler也只被调用一次,最快速的得到一个图像结果,可能会牺牲图像质量。
- .Opportunistic,resultHandler会被调用多次,会先提供低质量的图像以临时显示,随后会将指定尺寸的图像返回。如果指定尺寸的高质量的图像有缓存,那么直接返回高质量的图像。
*/
1.2 PHImageRequestOptionsDeliveryMode。
//请求的图像质量和交付优先级。只用这个属性将告诉Photos要快速提供图像(可能牺牲图像质量)、提供高质量图像(可能牺牲速度)、系统自动选择
typedef NS_ENUM(NSInteger, PHImageRequestOptionsDeliveryMode) {
PHImageRequestOptionsDeliveryModeOpportunistic = 0, // 为了平衡图像质量和响应速度,Photos会提供一个或者多个结果
PHImageRequestOptionsDeliveryModeHighQualityFormat = 1, // 只提供高质量图像、无论他需要多少时间加载
PHImageRequestOptionsDeliveryModeFastFormat = 2 // 最快速的得到一个图像结果,可能会牺牲图像质量。
};
1.3 PHImageRequestOptionsVersion。
//请求的图片版本。使用这个属性请求图片的不带编辑的版本,或则请求一个高质量的原始数据。
typedef NS_ENUM(NSInteger, PHImageRequestOptionsVersion) {
PHImageRequestOptionsVersionCurrent = 0, // 图片的最新版本(包括所有编辑版本)
PHImageRequestOptionsVersionUnadjusted, // 原版、无任何编辑版本
PHImageRequestOptionsVersionOriginal // 原始的高保真的版本
} PHOTOS_ENUM_AVAILABLE_IOS_TVOS(8_0, 10_0);
1.4 PHImageRequestOptionsResizeMode。
// 对请求的图像怎样缩放。使用此属性可选择在请求图像数据时将图像与目标大小如何适应。
typedef NS_ENUM(NSInteger, PHImageRequestOptionsResizeMode) {
PHImageRequestOptionsResizeModeNone = 0, // 不做任何调整
PHImageRequestOptionsResizeModeFast, // 最快速的调整图像大小,有可能比给定大小略大
PHImageRequestOptionsResizeModeExact, // 保证与给定大小相等。如果使用normalizedCropRect属性,则必须指定为该模式。
};
1.5 normalizedCropRect。
// normalizedCropRect。是否对原始图像进行裁剪。如果要裁剪图像,请在坐标空间内指定要裁剪的区域,在坐标系内{0,0}点在图像左上角,{1.0,1.0}点在图像的右下角。这个属性默认值为CGRectZero,代表不裁剪,如果你指定了裁剪,那么必须对resizeMode属性设置为`PHImageRequestOptionsResizeModeExact `
增删改查
PHAsset
的修改需要使用:PHAssetChangeRequest,
'PHAssetCollection’的增删改查需要:PHAssetCollectionChangeRequest
'PHCollectionList’的增删改查需要:PHCollectionListChangeRequest
1、创建对应的修改请求request
2、操作的请求要求都在PHPhotoLibrary的performChanges(异步)或performChangesAndWait(同步)的changeBlock中执行
3、操作的结果通过completionHandler回调
实例:
新增一个album
PHPhotoLibrary.shared().performChanges({
// 新增一个相册
PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "新增相册")
}, completionHandler: { (success, error) in
})
新增一个PHAsset到指定相册
PHPhotoLibrary.shared().performChanges({
var collectionRequest = PHAssetCollectionChangeRequest.init(for: assetCollection)
var assetRequest: PHAssetChangeRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
// 返回一个可用的 placeholder 来代替“真实的” PHAsset 引用。
if let placeholder = assetRequest?.placeholderForCreatedAsset {
mediaLocalIdentifier = placeholder.localIdentifier
collectionRequest?.addAssets([placeholder] as NSArray)
}
}, completionHandler: { (success, error) in
})
修改监听
对相册发出变更请求后,系统会通知用户是否允许,用户允许后才会发生实质上的变化,系统会发布通知。
- 通过PHPhotoLibrary 注册一个观察者
PHPhotoLibrary.shared().register(self)
- 监听到变化更新结果
1、通过changeDetails() 检查PHFetchResult是否有变化
2、如果有变化通过.fetchResultAfterChanges() 获取变化后的结果集
3、如果hasIncrementalChanges为false或则hasMoves为true,这意味着旧的获取结果应该全部被新的值代替。直接执行reloadData操作
4、需要按照removedIndexes,insertedIndexes,changedIndexes的顺序,对列表进行deleteItems,insertItems,reloadItems刷新
优化滚动性能(使用缓存PHCachingImageManager)
为什么使用缓存?
当用户正在有大量资源的 collection 视图上极其快速的滑动时,不使用缓存管理器会影响滑动的表现效果(例如卡顿,缩略图加载慢等问题)。所以使用缓存行为是极其重要的。
缓存的实现:
滚动一系列缩略图时,我们可以在可视区域前后维护一些数据缓存。如图:
- 生成PHCachingImageManager实例
- 照片资源比较多的相册我们可以设置allowsCachingHighQualityImages为false(默认为true缓存高质量的图。具体可以视情况而定:例如图片资源比较少的相册可以设置为true)
- 获取将要出现和消失的窗口,分别调用方法startCachingImagesForAssets:targetSize:contentMode:options:(预先将一些图像加载到内存中)和stopCachingImagesForAssets:targetSize:contentMode:options:(停止缓存特定资源列表) 并指定 目标尺寸target size,内容模式content mode,选项options参数
- 需要从asset对象中获取图片的时候,调用
requestImageForAsset:targetSize:contentMode:options:resultHandler (需要注意targetSize,contentMode,options这些参数值需要与缓存设置的参数保持一致)
当图像即将要展示在屏幕上时,比如当要在一组滚动的 collection 视图上展示大量的资源图像的缩略图时,预先将一些图像加载到内存中有时是非常有用的。PhotoKit 提供了一个 PHImageManager 的子类来处理这种特定的使用场景 —— PHImageCachingManager。
allowsCachingHighQualityImages 属性可以让你指定图像管理器是否应该准备高质量图像。当缓存一个较短和不变的资源列表时,默认 true 的缓存高质量的图。当照片资源比较多用户快速滑动 为了避免卡顿和封面图加载慢的问题最好将它设置成 false 。(可以视情况而定,例如图片资源比较少的相册可以设置为true)
// 开始预缓存
self.imageManager = [[PHCachingImageManager alloc] init];
imageManager.startCachingImages(for: addedAssets,
targetSize: thumbnailSize , contentMode: .aspectFill, options: nil)
contentMode:PHImageContentModeAspectFill
options:nil];
// 停止缓存
imageManager.stopCachingImages(for: removedAssets,
targetSize: thumbnailSize , contentMode: .aspectFill, options: nil)
// 从缓存中读取相片
// Request an image for the asset from the PHCachingImageManager.
self.imageManager.requestImage(for: asset, targetSize: self.thumbnailSize, contentMode: .aspectFill, options: nil) { (image, info) in
}
官方demo中计算缓存区域的算法(通过可见区域找到将要显示和将要隐藏的区域,进行数据缓存和停止缓存)
- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler {
if (CGRectIntersectsRect(newRect, oldRect)) {
CGFloat oldMaxY = CGRectGetMaxY(oldRect);
CGFloat oldMinY = CGRectGetMinY(oldRect);
CGFloat newMaxY = CGRectGetMaxY(newRect);
CGFloat newMinY = CGRectGetMinY(newRect);
if (newMaxY > oldMaxY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
addedHandler(rectToAdd);
}
if (oldMinY > newMinY) {
CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
addedHandler(rectToAdd);
}
if (newMaxY < oldMaxY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
removedHandler(rectToRemove);
}
if (oldMinY < newMinY) {
CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
removedHandler(rectToRemove);
}
} else {
addedHandler(newRect);
removedHandler(oldRect);
}
}