iOS 下的相册与图片处理
需求
很多公司项目中都会使用到相册,以及相机,保存图片,从相册中选取图片等等操作。本文将详细介绍该功能如何实现优化,以及使用一些优秀的第三方库来辅助完成我们的需求。
photos framework 的使用
Photos Framework reference
Classes
PHAdjustmentData
/*
When a user edits an asset, Photos saves a PHAdjustmentData
object along with the modified image or video data.
在用户编辑一个 asset 时,相册会存储该 asset 在修改 image 或者 video 数
据的过程中
*/
PHAdjustmentData
什么是 asset?
PHAsset
/*
A PHAsset object represents an image or video file that appears
in the Photos app, including iCloud Photos content.
一个 PHAsset 对象代表相册中或者云存储中的一个 image 或者 video 文件。
*/
PHAssetChangeRequest
/*
You create and use PHAssetChangeRequest objects within a photo
library change block to create, delete, or modify PHAsset
objects.
当你在相册中增删改 PHAsset 对象时需要使用 PHAssetChangeRequest 对象
*/
PHAssetCreationRequest
/*
A PHAssetCreationRequest object, used within a photo library
change block, constructs a new photo or video asset from data
resources, and adds it to the Photos library.
PHAssetCreationRequest 对象用于在照片库的增删改操作中,创建一个新的
image 和 video asset 从 data resources 中,然后将其加入 photos
library 中。
*/
PHAssetCollectionChangeRequest
/*
You create and use PHAssetCollectionChangeRequest objects
within a photo library change block to create, delete, or
modify PHAssetCollection objects.
当你对 photo library 即 assetCollection 进行增删改操作时,使用
PHAssetCollectionChangeRequest 对象
*/
PHAssetResourceCreationOptions
/*
You use a PHAssetResourceCreationOptions object to specify
options when creating a new asset from data resources with a
PHAssetCreationRequest object.
通过 PHAssetResourceCreationOptions 对象来指定当创建一个
新的 asset 的 options。
*/
PHAssetResourceManager
/*
The shared PHAssetResourceManager object provides methods for
accessing the underlying data storage for the resources
associated with a Photos asset.
PHAssetResourceManager 对象提供方法访问关联相机 asset 资源的基础数据存
储
*/
PHAssetCollection
/*
A PHAssetCollection object represents a collection of photo or
video assets.
一个 PHAssetCollection 对象就代表一个相册
*/
重点:
PHPhotoLibrary
/*
The shared PHPhotoLibrary object represents the user’s Photos
library—the entire set of assets and collections managed by the
Photos app, including objects stored on the local device and
(if enabled) in iCloud Photos.
公共的 PHPhotoLibrary 对象代表用户的相册库,所有的图片和相册管理都
要 PHPhotoLibrary 管理。
*/
保存图片到自定义相册
保存图片一般分为三个步骤:
保存图片到【相机胶卷】
拥有一个【自定义相册】
添加刚才保存的图片到【自定义相册】
需要用到的框架和函数:
c语言函数:只能完成第一个步骤,比较简单
AssetsLibrary 框架:有 bug
Photos 框架 :iOS8以后可以使用
如果单纯的只需要完成第一步则推荐使用一个 c 语言函数就可以搞定。但是如果要保存图片到自定义相册,则需要使用框架。iOS7以前使用 AssetsLibrary 框架,但是该框架的稳定性不高。而 iOS 8 以后的 Photos 框架将会取代 AssetsLibrary 框架完成这一功能。根据目前 app 市场的版本占有率,推荐使用 Photos。
将图片保存到相机胶卷(c 语言函数实现)
/**
* 该 c 语言函数是将图片保存到相机胶卷中
* 第一个参数:image 图片
* 第二个参数:target
* 第三个参数:selector 方法名规定使用以下方法名 :- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo;
* 第四个参数:保存完毕后会将第四个参数传给函数调用者
* 方法作用:将图片保存到相机胶卷中,保存完毕后会调用 target 的 selector 方法
*/
UIImageWriteToSavedPhotosAlbum(self.imageView.image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo
{
}
如果第三个参数方法名没有按照规范,则有可能会报如下错误:
/*
错误信息:-[NSInvocation setArgument:atIndex:]: index (2) out of bounds [-1, 1]
错误解释:参数越界错误,方法的参数个数和实际传递的参数个数不一致
*/
保存图片到相机胶卷(使用 Photos 框架)
由上面 Photos 框架的介绍可以看出,我们要对相册中的 asset 进行增删改操作时,用到 PHAssetChangeRequest 类,而在 PHAssetChangeRequest 的头文件可以看到,想要在相册胶卷中保存图片。要用到下面方法:
[PHAssetChangeRequest creationRequestForAssetFromImage:self.imageView.image];
/*
但是单独使用时,程序会报错。错误如下:
错误信息:This method can only be called from inside of -
[PHPhotoLibrary performChanges:completionHandler:] or -
[PHPhotoLibrary performChangesAndWait:error:]
错误信息说的很清楚,这个方法只能在 PHPhotoLibrary 类中的这两个方法中
使用。
在 iOS app 中,任何对 photos 的增删改操作,都一定会放在上述错误信息的
两个方法中。
*/
具体代码如下:
/**
* 该方法是异步执行的,不会阻塞当前线程,而且执行完后会来到
* completionHandler 的 block 中。
*/
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:self.imageView.image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
}];
/**
* 该方法是同步执行的。在当前线程 如果执行失败 error 将会有值。
*/
NSError *error = nil;
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:self.imageView.image];
} error:&error];
上述代码执行的操作是:将图片保存到相机胶卷中。
拥有一个自定义相册
通过对 photos 框架的了解,我们知道,创建一个自定义相册,我们需要用到
PHAssetCollectionChangeRequest 类。而进入其头文件发现,仍然必须在上述的 PHPhotoLibrary 类的两个 block 中执行操作。
代码如下:
#pragma mark - 使用 photo 框架创建自定义名称的相册 并获取自定义到自定义相册
#pragma mark -
- (PHAssetCollection *)createCustomAssetCollection
{
// 获取 app 名称
NSString *title = [NSBundle mainBundle].infoDictionary[(NSString *)kCFBundleNameKey];
NSError *error = nil;
// 查找 app 中是否有该相册 如果已经有了 就不再创建
/**
* 参数一 枚举:
* PHAssetCollectionTypeAlbum = 1, 用户自定义相册
* PHAssetCollectionTypeSmartAlbum = 2, 系统相册
* PHAssetCollectionTypeMoment = 3, 按时间排序的相册
*
* 参数二 枚举:PHAssetCollectionSubtype
* 参数二的枚举有非常多,但是可以根据识别单词来找出我们想要的。
* 比如:PHAssetCollectionTypeSmartAlbum 系统相册 PHAssetCollectionSubtypeSmartAlbumUserLibrary 用户相册 就能获取到相机胶卷
* PHAssetCollectionSubtypeAlbumRegular 常规相册
*/
PHFetchResult<PHAssetCollection *> *result = [PHAssetCollection fetchAssetCollectionsWithType:(PHAssetCollectionTypeAlbum)
subtype:(PHAssetCollectionSubtypeAlbumRegular)
options:nil];
for (PHAssetCollection *collection in result) {
if ([collection.localizedTitle isEqualToString:title]) { // 说明 app 中存在该相册
return collection;
}
}
/** 来到这里说明相册不存在 需要创建相册 **/
__block NSString *createdCustomAssetCollectionIdentifier = nil;
// 创建和 app 名称一样的 相册
/**
* 注意:这个方法只是告诉 photos 我要创建一个相册,并没有真的创建
* 必须等到 performChangesAndWait block 执行完毕后才会
* 真的创建相册。
*/
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
PHAssetCollectionChangeRequest *collectionChangeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:title];
/**
* collectionChangeRequest 即使我们告诉 photos 要创建相册,但是此时还没有
* 创建相册,因此现在我们并不能拿到所创建的相册,我们的需求是:将图片保存到
* 自定义的相册中,因此我们需要拿到自己创建的相册,从头文件可以看出,collectionChangeRequest
* 中有一个占位相册,placeholderForCreatedAssetCollection ,这个占位相册
* 虽然不是我们所创建的,但是其 identifier 和我们所创建的自定义相册的 identifier
* 是相同的。所以想要拿到我们自定义的相册,必须保存这个 identifier,等 photos app
* 创建完成后通过 identifier 来拿到我们自定义的相册
*/
createdCustomAssetCollectionIdentifier = collectionChangeRequest.placeholderForCreatedAssetCollection.localIdentifier;
} error:&error];
// 这里 block 结束了,因此相册也创建完毕了
if (error) {
NSLog(@"创建相册失败");
}
return [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[createdCustomAssetCollectionIdentifier] options:nil].firstObject;
}
将相机胶卷的相片存储到自定义相册中
在 photos 框架中,我们并不能直接拿到相册 PHAssetCollection 类来进行增删改操作,需要一个中间类也就是上面所讲的 PHAssetCollectionChangeRequest 类。 步骤如下:
先通过 PHAssetCollection 对象创建 PHAssetCollectionChangeRequest对象
通过 PHAssetCollectionChangeRequest 对象来进行增删改操作。
#pragma mark - 将图片保存到自定义相册中
#pragma mark -
- (void)saveImageToCustomAlbum
{
// 将图片保存到相机胶卷
NSError *error = nil;
__block PHObjectPlaceholder *placeholder = nil;
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
placeholder = [PHAssetChangeRequest creationRequestForAssetFromImage:self.imageView.image].placeholderForCreatedAsset;
} error:&error];
if (error) {
NSLog(@"保存失败");
}
// 获取自定义相册
PHAssetCollection *createdCollection = [self createCustomAssetCollection];
// 将图片保存到自定义相册
/**
* 必须通过中间类,PHAssetCollectionChangeRequest 来完成
* 步骤:1.首先根据相册获取 PHAssetCollectionChangeRequest 对象
* 2.然后根据 PHAssetCollectionChangeRequest 来添加图片
* 这一步的实现有两个思路:1.通过上面的占位 asset 的标识来获取 相机胶卷中的 asset
* 然后,将 asset 添加到 request 中
* 2.直接将 占位 asset 添加到 request 中去也是可行的
*/
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:createdCollection];
// [request addAssets:@[placeholder]]; 下面的方法可以将最新保存的图片设置为封面
[request insertAssets:@[placeholder] atIndexes:[NSIndexSet indexSetWithIndex:0]];
} error:&error];
if (error) {
NSLog(@"保存失败");
} else {
NSLog(@"保存成功");
}
}
最终代码
最终代码:整理后可用于项目中
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 获取 当前 App 对 phots 的访问权限
PHAuthorizationStatus OldStatus = [PHPhotoLibrary authorizationStatus];
// 检查访问权限 当前 App 对相册的检查权限
/**
* PHAuthorizationStatus
* PHAuthorizationStatusNotDetermined = 0, 用户还未决定
* PHAuthorizationStatusRestricted, 系统限制,不允许访问相册 比如家长模式
* PHAuthorizationStatusDenied, 用户不允许访问
* PHAuthorizationStatusAuthorized 用户可以访问
* 如果之前已经选择过,会直接执行 block,并且把以前的状态传给你
* 如果之前没有选择过,会弹框,在用户选择后调用 block 并且把用户的选择告诉你
* 注意:该方法的 block 在子线程中运行 因此,弹框什么的需要回到主线程执行
*/
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
dispatch_async(dispatch_get_main_queue(), ^{
if (status == PHAuthorizationStatusAuthorized) {
// [self cSaveToCameraRoll];
// [self photoSaveToCameraRoll];
// [self fetchCameraRoll];
// [self createCustomAssetCollection];
// [self createdAsset];
// [self saveImageToCustomAlbum2];
[self saveImageToCustomAlbum1];
} else if (OldStatus != PHAuthorizationStatusNotDetermined && status == PHAuthorizationStatusDenied) {
// 用户上一次选择了不允许访问 且 这次又点击了保存 这里可以适当提醒用户允许访问相册
}
});
}];
}
#pragma mark - 将图片保存到自定义相册中 第一种写法 比较规范
#pragma mark -
- (void)saveImageToCustomAlbum1
{
// 获取保存到相机胶卷中的图片
PHAsset *createdAsset = [self createdAssets].firstObject;
if (createdAsset == nil) {
NSLog(@"保存图片失败");
}
// 获取自定义相册
PHAssetCollection *createdCollection = [self createCustomAssetCollection];
if (createdCollection == nil) {
NSLog(@"创建相册失败");
}
NSError *error = nil;
// 将图片保存到自定义相册
/**
* 必须通过中间类,PHAssetCollectionChangeRequest 来完成
* 步骤:1.首先根据相册获取 PHAssetCollectionChangeRequest 对象
* 2.然后根据 PHAssetCollectionChangeRequest 来添加图片
* 这一步的实现有两个思路:1.通过上面的占位 asset 的标识来获取 相机胶卷中的 asset
* 然后,将 asset 添加到 request 中
* 2.直接将 占位 asset 添加到 request 中去也是可行的
*/
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:createdCollection];
// [request addAssets:@[placeholder]];
[request insertAssets:@[createdAsset] atIndexes:[NSIndexSet indexSetWithIndex:0]];
} error:&error];
if (error) {
NSLog(@"保存失败");
} else {
NSLog(@"保存成功");
}
}
#pragma mark - 获取保存到【相机胶卷】的图片
#pragma mark -
- (PHFetchResult<PHAsset *> *)createdAssets
{
// 将图片保存到相机胶卷
NSError *error = nil;
__block NSString *assetID = nil;
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
assetID = [PHAssetChangeRequest creationRequestForAssetFromImage:self.imageView.image].placeholderForCreatedAsset.localIdentifier;
} error:&error];
if (error) return nil;
return [PHAsset fetchAssetsWithLocalIdentifiers:@[assetID] options:nil];
}
#pragma mark - 使用 photo 框架创建自定义名称的相册 并获取自定义到自定义相册
#pragma mark -
- (PHAssetCollection *)createCustomAssetCollection
{
// 获取 app 名称
NSString *title = [NSBundle mainBundle].infoDictionary[(NSString *)kCFBundleNameKey];
NSError *error = nil;
// 查找 app 中是否有该相册 如果已经有了 就不再创建
/**
* 参数一 枚举:
* PHAssetCollectionTypeAlbum = 1, 用户自定义相册
* PHAssetCollectionTypeSmartAlbum = 2, 系统相册
* PHAssetCollectionTypeMoment = 3, 按时间排序的相册
*
* 参数二 枚举:PHAssetCollectionSubtype
* 参数二的枚举有非常多,但是可以根据识别单词来找出我们想要的。
* 比如:PHAssetCollectionTypeSmartAlbum 系统相册 PHAssetCollectionSubtypeSmartAlbumUserLibrary 用户相册 就能获取到相机胶卷
* PHAssetCollectionSubtypeAlbumRegular 常规相册
*/
PHFetchResult<PHAssetCollection *> *result = [PHAssetCollection fetchAssetCollectionsWithType:(PHAssetCollectionTypeAlbum)
subtype:(PHAssetCollectionSubtypeAlbumRegular)
options:nil];
for (PHAssetCollection *collection in result) {
if ([collection.localizedTitle isEqualToString:title]) { // 说明 app 中存在该相册
return collection;
}
}
/** 来到这里说明相册不存在 需要创建相册 **/
__block NSString *createdCustomAssetCollectionIdentifier = nil;
// 创建和 app 名称一样的 相册
/**
* 注意:这个方法只是告诉 photos 我要创建一个相册,并没有真的创建
* 必须等到 performChangesAndWait block 执行完毕后才会
* 真的创建相册。
*/
[[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
PHAssetCollectionChangeRequest *collectionChangeRequest = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:title];
/**
* collectionChangeRequest 即使我们告诉 photos 要创建相册,但是此时还没有
* 创建相册,因此现在我们并不能拿到所创建的相册,我们的需求是:将图片保存到
* 自定义的相册中,因此我们需要拿到自己创建的相册,从头文件可以看出,collectionChangeRequest
* 中有一个占位相册,placeholderForCreatedAssetCollection ,这个占位相册
* 虽然不是我们所创建的,但是其 identifier 和我们所创建的自定义相册的 identifier
* 是相同的。所以想要拿到我们自定义的相册,必须保存这个 identifier,等 photos app
* 创建完成后通过 identifier 来拿到我们自定义的相册
*/
createdCustomAssetCollectionIdentifier = collectionChangeRequest.placeholderForCreatedAssetCollection.localIdentifier;
} error:&error];
// 这里 block 结束了,因此相册也创建完毕了
if (error) return nil;
return [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[createdCustomAssetCollectionIdentifier] options:nil].firstObject;
}
从相册中选取图片
从相册里面选择图片到 App 中
选择单张图片
UIImagePickerController
AssetsLibrary 框架
Photos 框架
选择多张图片(图片数量 >= 2)
AssetsLibrary 框架
Photos 框架
利用照相机拍一张照片到 App
使用 UIImagePickerController (界面已经写好了)
AVCaptureSession (AVFoundation框架下)
使用第三方框架获取多张照片
这里推荐大家使用 CTAssetsPickerController 第三方框架。
地址如下:CTAssetsPickerController
https://github.com/chiunam/CTAssetsPickerController
如果您的项目是使用 pod 来管理第三方库的,直接
pod ‘CTAssetsPickerController’ 就可以了。其内部还依赖一个叫做 PureLayout 的库。
具体信息见代码:多图片访问
https://github.com/lizhaoLoveIT/assetPhotoPicker