iOS照片框架-PhotoKit

11,120 阅读12分钟

1.介绍

PhotoKitApp在使用、管理图片和视频的框架,而且还包括了iCloud上面的图片以及实时照片.

2.概要

  • iOS中,PhotoKit支持应用构建照片以及编辑扩展,还可以直接访问管理照片和视频元资源以及元资源集合例如专辑,时刻和共享相册.

3.官方Demo-PhotoBrowse

Browsing and Modifying Photo Albums

此示例演示如何使用自定义实现类似的布局.它使用PhotoKit获取资源缩略图,然后将其显示为单个照片,视频或动态图片。此外示例应用程序PhotoBrowse还演示了如何将用户的照片整理到相册和内置集合中,例如最近添加的和收藏夹.它支持专辑的创建,删除,修改,以及个人资源的编辑和收藏.

3.1 获取所有相册,所有照片请求

    let allPhotosOptions = PHFetchOptions()
    allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
    // 获取所有照片
    allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
    // 获取智能相册
    smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .albumRegular, options:     nil)
    // 获取用户创建的所有相册
    userCollections = PHCollectionList.fetchTopLevelUserCollections(with: nil)

获取操作是由上面描述的实体的类方法实现的.要使用哪个类/方法,取决于问题所在范围和你展示与遍历照片库的方式.所有获取方法的命名都是相似的:class func fetchXXX(..., options: PHFetchOptions) -> PHFetchResult .options参数给了我们一个对结果进行过滤和排序的途径,这和 NSFetchRequestpredicatesortDescriptors参数类似.

3.2 观察变化

首先,你需要通过共享的PHPhotoLibrary对象,用registerChangeObserver(...)方法注册一个变化观察者 (这个观察者要遵从PHPhotoLibraryChangeObserver协议).只要另一个应用或者用户在照片库中做的修改影响了你在变化前获取的任何资源或资源集合的话,变化观察者的photoLibraryDidChange(...)方法都会被调用.这个方法只有一个 PHChange类型的参数,你可以用它来验证这些变化是否和你所感兴趣的获取对象有关联.

更新获取的结果PHChange提供了几个方法,让你可以通过传入任何你感兴趣的PHObject对象或PHFetchResult 对象来追踪它们的变化.这几个方法是changeDetailsForObject(...)changeDetailsForFetchResult(...) .如果没有任何变化,这些方法会返回nil,否则你可以借助PHObjectChangeDetailsPHFetchResultChangeDetails 对象来观察这些变化。

PHObjectChangeDetails提供了一个对最新的照片实体对象的引用,以及告诉你对象的图像数据是否曾变化过、对象是否曾被删除过的布尔值.PHFetchResultChangeDetails封装了施加在你之前通过获取所得到的PHFetchResult 上的变化的信息.PHFetchResultChangeDetails是为了尽可能简化CollectionViewTableView 的更新操作而设计的.它的属性恰好映射到你在使用一个典型的CollectionViewupdate handler 时所需要的信息.注意,若要正确的更新UITableView/UICollectionView,你必须以正确顺序来处理变化,那就是:RICE —— removedIndexes,insertedIndexes,changedIndexes,enumerateMovesWithBlock (如果hasMovestrue 的话).另外,PHFetchResultChangeDetailshasIncrementalChanges属性可以被设置成 false,这意味着旧的获取结果应该全部被新的值代替.这种情况下,你应该调用1UITableView/UICollectionView1的 reloadData.

注意:没有必要以集中的方式处理变化.如果你应用中的多个组件需要处理照片实体,那么它们每个都要有自己的 PHPhotoLibraryChangeObserver.接着组件就能靠自己查询PHChange对象,检测是否需要 (以及如何)更新它们自己的状态。

    // 注册监听,获取相册数据源变化,系统提供回调方法-photoLibraryDidChange
    PHPhotoLibrary.shared().register(self)

    func photoLibraryDidChange(_ changeInstance: PHChange) {
        // 接收通知可能会在后台线程,所以此处的UI更新需放于主线程调用
        DispatchQueue.main.sync {
            // Check each of the three top-level fetches for changes.
            if let changeDetails = changeInstance.changeDetails(for: allPhotos) {
                // 更新缓存的数据源
                allPhotos = changeDetails.fetchResultAfterChanges
            }
            
            // 更新缓存的数据源并更新UI
            if let changeDetails = changeInstance.changeDetails(for: smartAlbums) {
                smartAlbums = changeDetails.fetchResultAfterChanges
                tableView.reloadSections(IndexSet(integer: Section.smartAlbums.rawValue), with: .automatic)
            }
            if let changeDetails = changeInstance.changeDetails(for: userCollections) {
                userCollections = changeDetails.fetchResultAfterChanges
                tableView.reloadSections(IndexSet(integer: Section.userCollections.rawValue), with: .automatic)
            }
        }
    }
    
    // 取消监听
    PHPhotoLibrary.shared().unregisterChangeObserver(self)

3.3 缓存以及展示列表缩略图

当图像即将要展示在屏幕上时,比如当要在一组滚动的collection视图上展示大量的资源图像的缩略图时,预先将一些图像加载到内存中有时是非常有用的.PhotoKit提供了一个PHImageManager的子类来处理这种特定的使用场景 —— PHImageCachingManager.

PHImageCachingManager提供了一个关键方法startCachingImagesForAssets(...)你传入一个PHAssets类型的数组,一些请求参数,以及一些请求单个图像时即将用到的可选项.此外,还有一些方法可以让你通知缓存管理器来停止缓存特定资源列表,以及停止缓存所有图像.

默认情况下,如果图像管理器决定要用最优策略,那么它会在将图像的高质量版本递送给你之前,先传递一个较低质量的版本.你可以通过deliveryMode属性来控制这个行为;上面所描述的默认行为的值为 .Opportunistic.如果你只想要高质量的图像,并且可以接受更长的加载时间,那么将属性设置为 .HighQualityFormat.如果你想要更快的加载速度,且可以牺牲一点图像质量,那么将属性设置为.FastFormat.

你可以使用PHImageRequestOptionssynchronous属性,让requestImage...系列的方法变成同步操作。注意:当 synchronous设为true时,deliveryMode属性就会被忽略,并被当作.HighQualityFormat来处理.在设置这些参数时,一定要考虑到你的一些用户有可能开启了iCloud照片库,这点非常重要.PhotoKitAPI不一定会对设备的照片和 iCloud上照片进行区分 —— 它们都用同一个requestImage方法来加载.这意味着任意一个图像请求都有可能是一个通过蜂窝网络来进行的非常缓慢的网络请求.当你要用.HighQualityFormat或者做一个同步请求的时候,要牢记这个.注意:如果你想要确保请求不经过网络,可以将 networkAccessAllowed设为false.另一个和iCloud相关的属性是 progressHandler.你可以将它设为一个PHAssetImageProgressHandlerblock,当从iCloud下载照片时,它就会被图像管理器自动调用。

这里等页面展示时预先缓存数据后再取数据后展示.

let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect)
let addedAssets = addedRects
    .flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
    .map { indexPath in fetchResult.object(at: indexPath.item) }
let removedAssets = removedRects
    .flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
    .map { indexPath in fetchResult.object(at: indexPath.item) }

// 更新缓存数据
imageManager.startCachingImages(for: addedAssets,
                                targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
imageManager.stopCachingImages(for: removedAssets,
                               targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GridViewCell", for: indexPath) as? GridViewCell
    else { fatalError("Unexpected cell in collection view") }

// 给动态照片Cell添加一个badge
if asset.mediaSubtypes.contains(.photoLive) {
    cell.livePhotoBadgeImage = PHLivePhotoView.livePhotoBadgeImage(options: .overContent)
}

// 通过PHCachingImageManager请求照片
cell.representedAssetIdentifier = asset.localIdentifier
imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { image, _ in
    // UIKit may have recycled this cell by the handler's activation time.
    // 只有请求到通用类型标识符一致的资源时才配置图片
    if cell.representedAssetIdentifier == asset.localIdentifier {
        cell.thumbnailImage = image
    }
})

3.4 编辑修改资源文件

PhotoKit在照片库做改变,说到底其实是先创建了一个链接到某个资源或者资源集合的变化请求对象,再设置请求对象的相关属性或调用合适的方法来描述你想要提交的变化.这个必须通过performChanges(...)方法,在提交到共享的PHPhotoLibraryblock内完成.注意:你需要准备好在performChanges方法的completion block 里处理失败的情况.虽然处理的是能被多个参与者(如你的应用,用户,其他应用,照片扩展等)改变的状态,但这个方式能提供安全性,也相对易用.

想要修改资源,需要创建一个PHAssetChangeRequest,然后你就可以修改创建创建日期,资源位置,以及是否将隐藏资源,是否将资源看做用户收藏等.此外,你还可以从用户的库里删除资源.类似地,若要修改资源集合或集合列表,需要创建一个 PHAssetCollectionChangeRequestPHCollectionListChangeRequest对象.然后你就可以修改集合标题,添加或删除集合成员,或者完全删除集合.

在你的变化提交到用户照片库前,系统会向用户展示一个明确的获取权限的警告框.

3.4.1 修改资源

DispatchQueue.global(qos: .userInitiated).async {
    // 创建调整的数据源.
    // 如果你对一张照片进行了滤镜处理,你应该会创建一个adjustmentData对象,它记录了用户选择了什么滤镜、相关配置的参数、以及使用这(款/些)滤镜的命令.接下去,用户可以用你的app或者别的可以获取你的adjustmentData对象的app来恢复这张照片到初始状态
    let adjustmentData = PHAdjustmentData(formatIdentifier: self.formatIdentifier,
                                          formatVersion: self.formatVersion,
                                          data: filterName.data(using: .utf8)!)
    
    // 创建输出数据源.
    let output = PHContentEditingOutput(contentEditingInput: input)
    output.adjustmentData = adjustmentData
    
    // 设置滤镜类型.
    let applyFunc: (String, PHContentEditingInput, PHContentEditingOutput, @escaping () -> Void) -> Void
    if self.asset.mediaSubtypes.contains(.photoLive) {
        applyFunc = self.applyLivePhotoFilter
    } else if self.asset.mediaType == .image {
        applyFunc = self.applyPhotoFilter
    } else {
        applyFunc = self.applyVideoFilter
    }
    
    // 使用滤镜.
    applyFunc(filterName, input, output, {
        // 当滤镜提交使用后,再更新到资源库.
        PHPhotoLibrary.shared().performChanges({
            let request = PHAssetChangeRequest(for: self.asset)
            request.contentEditingOutput = output
        }, completionHandler: { success, error in
            if !success { print("Can't edit the asset: \(String(describing: error))") }
        })
    })
}

3.4.2 修改资源集合或集合列表

PHPhotoLibrary.shared().performChanges({
    PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: title)
}, completionHandler: { success, error in
    if !success { print("Error creating album: \(String(describing: error)).") }
})

3.4.3 添加新资源

PHPhotoLibrary.shared().performChanges({
    let creationRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
    if let assetCollection = self.assetCollection {
        let addAssetRequest = PHAssetCollectionChangeRequest(for: assetCollection)
        addAssetRequest?.addAssets([creationRequest.placeholderForCreatedAsset!] as NSArray)
    }
}, completionHandler: {success, error in
    if !success { print("Error creating the asset: \(String(describing: error))") }
})

3.4.4 删除资源

if assetCollection != nil {
    // 从当前选中的相册|集合中删除资源
    PHPhotoLibrary.shared().performChanges({
        let request = PHAssetCollectionChangeRequest(for: self.assetCollection)!
        request.removeAssets([self.asset] as NSArray)
    }, completionHandler: completion)
} else {
    // 直接从资源库中删除.
    PHPhotoLibrary.shared().performChanges({
        PHAssetChangeRequest.deleteAssets([self.asset] as NSArray)
    }, completionHandler: completion)
}

3.5 其他

判断是否是Favorite的相册,以及添加Favorite

PHPhotoLibrary.shared().performChanges({
    let request = PHAssetChangeRequest(for: self.asset)
    request.isFavorite = !self.asset.isFavorite
}, completionHandler: { success, error in
    if success {
        DispatchQueue.main.sync {
            sender.title = self.asset.isFavorite ? "♥︎" : "♡"
        }
    } else {
        print("Can't mark the asset as a Favorite: \(String(describing: error))")
    }
})

4.API介绍

4.1 PhotoKit框架常用类简介图

4.2 PhotoKit框架构成图

4.3 常用类

原文: iOS照片框架

4.3.1 基本

PHPhotoLibrary: 表示用户的照片库,用于请求、获取照片库的权限,监听照片库的变化;`

4.3.2 资源

PHObject: 抽象基类,其他PhotoKit类都继承该类,提供了一个localIdentifier属性;

PHAsset: 表示照片库中的一个单独的资源(可以是图片,也可以是视频;与ALAsset类似),用于获取、保存资源的元数据;PHAsset只包含元数据(如图片大小、创建日期等),具体的图片、视频数据需要使用PHImageManager进行加载; 若一个资源的representsBurst属性为true,则表示该资源是一系列连拍照片中的代表照片,可以通过fetchAssetsWithBurstIdentifier()方法,传入burstIdentifier属性,获取连拍照片中的剩余的其他照片;

PHCollection: PHAssetCollection和PHCollectionList的父类;

PHAssetCollection: 资源集合,表示成组的资源;可以表示照片库中的一个相册、时刻、智能相册; 智能相册:系统默认提供的特定相册,如最近删除、视频列表、收藏等;

PHCollectionList: 表示一组PHCollections的集合;其本身也是PHCollection,故PHCollectionList也可以包含其他的PHCollectionList;

4.3.3 获取

PHFetchResult: 表示一系列的PHAsset的结果集合,也可以是一系列的PHCollection的结果集合;

PHFetchOptions: 获取PHFetchResult时的传入参数,起过滤、排序等作用,可以过滤类型、日期、名称等; 传入nil则使用系统默认值;

PHImageManager: 用于加载资源;

PHCachingImageManager: 继承于PHImageManager,带有缓存的加载资源;当使用大量的资源时,可以先在后台准备资源图片,减少在之后的请求单个资源时的延迟;如当想要使用照片、视频资源的缩略图填充一个集合视图时就可以使用PHCachingImageManager;先使用startCachingImagesForAssets方法进行资源准备,之后还使用PHImageManager的request方法加载资源;

PHImageRequestOptions: 加载资源时的传入参数,控制资源的输出尺寸等规格;

4.3.4 更新

PHAssetChangeRequest: 用来创建、删除和修改PHAsset对象;

PHAssetCollectionChangeRequest: 用来创建、删除和修改PHAssetCollection对象;

PHCollectionListChangeRequest: 用来创建、删除和修改PHCollectionList对象;

PHAssetCreationRequest: PHAssetChangeRequest的子类,也可以用来创建,丰富了添加资源的方式;

4.3.5 相关枚举值

注意:获取指定类型的相册时,主类型和子类型要匹配,若不匹配则系统会按照any子类型处理; 对于moment类型,子类型使用any; 对于smartAlbum类型,子类型使用albumRegular比使用any多一个Recently Deleted(最近删除)的相册;

enum PHAssetCollectionType : Int {
    case album // 用户自己在Photos app中建立的相册、从iTunes同步来的相册
    case smartAlbum // Photos app内置的相册(内容动态更新)
    case moment // Photos app自动生成的时间、地点分组的相册
}
enum PHAssetCollectionSubtype : Int {
    // PHAssetCollectionTypeAlbum regular subtypes
    case albumRegular // 用户自己在Photos app中建立的相册
    case albumSyncedEvent // 已废弃;从iPhoto同步来的事件相册
    case albumSyncedFaces // 从iPhoto同步来的人物相册
    case albumSyncedAlbum // 从iPhoto同步来的相册
    case albumImported // 从相机或外部存储导入的相册
    // PHAssetCollectionTypeAlbum shared subtypes
    case albumMyPhotoStream // 用户的iCloud照片流
    case albumCloudShared // //用户使用iCloud共享的相册

    // PHAssetCollectionTypeSmartAlbum subtypes
    case smartAlbumGeneric // 非特殊类型的相册,从macOS Photos app同步过来的相册
    case smartAlbumPanoramas // 相机拍摄的全景照片的相册
    case smartAlbumVideos // 相机拍摄的视频的相册
    case smartAlbumFavorites // 收藏的照片、视频的相册
    case smartAlbumTimelapses // 延时视频的相册
    case smartAlbumAllHidden // 包含隐藏照片、视频的相册
    case smartAlbumRecentlyAdded // 相机近期拍摄的照片、视频的相册
    case smartAlbumBursts // 连拍模式拍摄的照片的相册
    case smartAlbumSlomoVideos // Slomo是slow motion的缩写,高速摄影慢动作解析(iOS设备以120帧拍摄)的相册
    case smartAlbumUserLibrary // 相机相册,包含相机拍摄的所有照片、视频,使用其他应用保存的照片、视频
    @available(iOS 9.0, *)
    case smartAlbumSelfPortraits
    @available(iOS 9.0, *)
    case smartAlbumScreenshots
    @available(iOS 10.2, *)
    case smartAlbumDepthEffect
    @available(iOS 10.3, *)
    case smartAlbumLivePhotos
    @available(iOS 11.0, *)
    case smartAlbumAnimated
    @available(iOS 11.0, *)
    case smartAlbumLongExposures
    // Used for fetching, if you don't care about the exact subtype
    case any // 包含所有类型
}

5.总结

iOS 8之前,开发者只能使用AssetsLibrary框架来处理照片,但随着苹果手机的不断更新,设备的不断进步,照片功能不断更新,原有的AssetsLibrary框架已经不再能跟得上脚步了.但随着 iOS 8 的到到来,苹果给我们提供了一个现代化的框架PhotoKit,使得开发者们能够更好的适应潮流,更好的开发相关应用,让应用与设备能够完美无缝的工作.

参考

Photos框架获取本地图片视频信息

照片框架

苹果官方文档中文译本之-PHAdjustmentData