首先几乎每个App都用到了自定义相册的展示,系统在iOS8以后提供了photos.framework来供我们与相册交互。既然要自定义相册,最先要做的就是封装获取相册相关的API。现在已经有很多开源的相册项目,为什么我们还要自己造轮子呢,其实大部分的开源项目都可以实现大部分的功能,但确实无法满足所有的需求,所以该踩的坑还是要踩,以下就结合我们项目中遇到的一些问题,分享一些tips。
1.相册权限的判断
当用户点击相册时,其实有三种情况。情况1,已经授权过了;情况2.之前拒绝授权;情况3,之前没有决定过。针对于情况1,我们直接打开就可以了。针对于情况2,我们可以弹框提示,然后跳转到系统设置里面,引导用户打开。针对于情况3,我们需要弹框,然后再用户做出选择之后,在根据是情况1、还是情况2来进行不同的操作。
+ (void)requestAlbumAuthorization:(void (^)(BOOL isAuthorized))block ifAlert:(BOOL)ifAlert
{
PHAuthorizationStatus photoAuthorStatus = [PHPhotoLibrary authorizationStatus];
if (photoAuthorStatus == PHAuthorizationStatusAuthorized)
block ? block(YES) : nil;
else if (photoAuthorStatus == PHAuthorizationStatusDenied)
{
if (ifAlert)
[AlertManager showAlertControllerWithType:AlertManagerTypeOpenAlbumFail];
block ? block(NO) : nil;
}
else
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (status == PHAuthorizationStatusAuthorized)
block ? block(YES) : nil;
else
{
if (ifAlert)
[AlertManager showAlertControllerWithType:AlertManagerTypeOpenAlbumFail];
block ? block(NO) : nil;
}
});
}];
}
2.如何判断相册资源的类型
首先我们知道每一个相册资源都是PHAsset对象,而系统提供了mediaType
这个属性来告诉我们判断该资源是图片、视频还是音频。但是针对于图片来说,又可能是静态图、GIF或者LIVE Photo。虽然又可以通过mediaSubtypes
是否等于 PHAssetMediaSubtypePhotoLive
来判断图片是否是LIVE Photo。但如何判断图片是否是GIF通过系统的API就无能为力了。
那么如何判断PHAsset
对象是不是GIF呢??
其实我们可以通过PHAsset
得到对应的资源对象PHAssetSource
,该对象的uniformTypeIdentifier
属性(即UTI)来判断是否是GIF(com.compuserve.gif)
- (BOOL)isImageAssetGIF:(PHAsset *)asset
{
PHAssetResource * resource = [[PHAssetResource assetResourcesForAsset:asset] firstObject];
return [resource.uniformTypeIdentifier isEqualToString:@"com.compuserve.gif"];
}
原理如此,但是获取PHAssetResource
太慢,如果放在主线程同步获取类型的话,会造成卡顿。这里可以通过KVC来获取uniformTypeIdentifier
。完整的获取类型代码如下:
+ (AssetMediaType)getTypeForAsset:(PHAsset *)asset
{
switch (asset.mediaType)
{
case PHAssetMediaTypeVideo:
return AssetMediaTypeVideo;
case PHAssetMediaTypeImage:
{
if (asset.mediaSubtypes == PHAssetMediaSubtypePhotoLive)
return AssetMediaTypeLivePhoto;
if ([[asset valueForKey:@"uniformTypeIdentifier"] isEqualToString:@"com.compuserve.gif"])
return AssetMediaTypeGIF;
return AssetMediaTypeStaticGraph;
}
default:
return AssetMediaTypeUnknown;
}
}
3.获取资源的缩略图
一般情况下,在某个相册的列表页,会展示大量的图片,而相册中的照片的分辨率几乎都是几千*几千的,而我们在列表页只需要展示宽度为100~200像素的图片即可,那么就需要我们去获取缩略图。获取缩略图的方法如下,具体注意事项可参考代码中的注释。
+ (PHImageRequestID)requestImageForAsset:(PHAsset *)asset size:(CGSize)size resizeMode:(PHImageRequestOptionsResizeMode)resizeMode needsDegrade:(BOOL)needsDegrade completion:(void (^)(UIImage * image, NSDictionary * info))completion
{
PHImageRequestOptions * option = [[PHImageRequestOptions alloc] init];
option.resizeMode = resizeMode; // resizeMode:对请求的图像怎样缩放。有三种选择:None,默认加载方式;Fast,尽快地提供接近或稍微大于要求的尺寸;Exact,精准提供要求的尺寸。针对于缩略图,我们使用Fast即可。
option.networkAccessAllowed = YES;//处理图片存在iCloud中的情况,将网络请求设置为打开,默认为NO
/*
info字典提供请求状态信息:
PHImageResultIsInCloudKey:图像是否必须从iCloud请求
PHImageResultIsDegradedKey:当前UIImage是否是低质量的,这个可以实现给用户先显示一个预览图
PHImageResultRequestIDKey和PHImageCancelledKey:请求ID以及请求是否已经被取消
PHImageErrorKey:如果没有图像,字典内的错误信息
*/
PHImageRequestID requestID = [[PHCachingImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFit options:option resultHandler:^(UIImage * _Nullable image, NSDictionary * _Nullable info) {
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey];
if (needsDegrade == NO)
downloadFinined = downloadFinined && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
// 如果不判断PHImageResultIsDegradedKey,即如果该图片在iCloud上时候,会先显示一张模糊的预览图,待加载完毕后会显示高清图
// && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined && completion)
{
//回调在主线程,所以直接返回即可
completion(image, info);
}
}];
return requestID;
}
4.获取预览页的资源
- 获取静态图的预览资源**
针对获取静态图的预览资源,我们可以通过上一条说的获取缩略图的方法来获取。++为保证快速滑动的流畅性,在快速滑动的时候获取图片宽度最大不要超过500++。 - 获取动态图的预览资源
针对于动态图,通过上一条方法获取的图片的方法可以获取成功,但是获取到的是静态图。如果希望图片可以播放,那么就需要获取图片的data,然后再解析data,播放GIF。++这里也有一个坑,如果GIF存在iCloud上,即便我们设置了networkAccessAllowed
为YES
,也可能会存在返回的图片是静态图的情况++。百思不得解,最后也是没有办法的办法,通过PHAssetResourceManager
的writeDataForAssetResource:toFile:options:completionHandler:
方法来获取图片的data。
+ (void)requestOriginalImageDataForAsset:(PHAsset *)asset completion:(void (^)(NSData *, NSDictionary *))completion
{
if (!completion)
return ;
PHImageRequestOptions * option = [[PHImageRequestOptions alloc] init];
option.networkAccessAllowed = YES;
option.resizeMode = PHImageRequestOptionsResizeModeFast;
AssetMediaType type = [self getTypeForAsset:asset];
[[PHImageManager defaultManager] requestImageDataForAsset:asset options:option resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
BOOL downloadFinined = ![[info objectForKey:PHImageCancelledKey] boolValue] && ![info objectForKey:PHImageErrorKey] && ![[info objectForKey:PHImageResultIsDegradedKey] boolValue];
if (downloadFinined)
{
if (type == AssetMediaTypeGif)
{
NSArray <PHAssetResource *> * resources = [PHAssetResource assetResourcesForAsset:asset];
PHAssetResource * resource = [resources objectAtIndex:0];
if (![resource.uniformTypeIdentifier isEqualToString:dataUTI])
{
// 如果请求的是GIF,返回的不是GIF,那么执行导出的操作
[self requestDataForAssetSource:resource completion:completion];
return ;
}
}
completion(imageData, info);
}
}];
}
+ (void)requestDataForAssetSource:(PHAssetResource *)resource completion:(void (^)(NSData *, NSDictionary *))completion
{
if (completion == nil)
return ;
PHAssetResourceRequestOptions * option = [[PHAssetResourceRequestOptions alloc] init];
option.networkAccessAllowed = YES;
NSString * exportFilePath = [self getExportFilePathForType:AssetMediaTypeGif];
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:resource toFile:[NSURL fileURLWithPath:exportFilePath] options:option completionHandler:^(NSError * _Nullable error) {
NSData * data = [NSData dataWithContentsOfFile:exportFilePath];
if (error)
data = nil;
if ([NSThread isMainThread])
completion(data, nil);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(data, nil);
});
// 得到data之后删除文件...
[[NSFileManager defaultManager] removeItemAtPath:exportFilePath error:nil];
}];
}
- 获取LIVE Photo的预览资源
获取LIVE Photo资源,感觉系统还是很友好,得到PHLivePhoto
对象之后,加载到对应的view上,3D Touch就能播放了。==其实这里也有坑!!!== 我来描述一下现象,通过iOS 11系统的iPhone X拍摄LIVE Photo,通过iCloud导入到iOS 10系统的iPhone 6s上,获取PHLivePhoto
对象的时候竟然崩溃了…原因貌似是系统在调用stringByAppendingPathExtension:
方法时参数传递了nil
,好吧,通过Method swizzle
来解决。
/**
为了解决LivePhoto获取时,stringByAppendingPathExtension参数为nil的情况,猜测原因可能是iOS11 iPhone X 拍的LivePhoto在iOS 10.3.3 iPhone 6s Plus上产生系统兼容的问题
*/
@implementation NSString (LivePhoto)
+ (void)load
{
[NSString swizzleInstanceMethod:@selector(stringByAppendingPathExtension:) withMethod:@selector(nil_stringByAppendingPathExtension:)];
}
- (NSString *)nil_stringByAppendingPathExtension:(NSString *)str
{
if (str == nil || [str isEqual:[NSNull null]])
return nil;
return [self nil_stringByAppendingPathExtension:str];
}
@end
@implementation AlbumTool
+ (void)requestLivePhotoForAsset:(PHAsset *)asset completion:(void (^)(PHLivePhoto *, NSDictionary *))completion
{
if (!completion)
return ;
PHLivePhotoRequestOptions * option = [[PHLivePhotoRequestOptions alloc] init];
if ([option respondsToSelector:@selector(setVersion:)])
option.version = PHImageRequestOptionsVersionCurrent;
// 这样保证很快,在目前480在我们项目上已经够用,并且下方resultHandler不会调用多次,如果有其他需求可以修改参数
option.deliveryMode = PHImageRequestOptionsDeliveryModeFastFormat;
option.networkAccessAllowed = YES;
[[PHCachingImageManager defaultManager] requestLivePhotoForAsset:asset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFit options:option resultHandler:^(PHLivePhoto * _Nullable livePhoto, NSDictionary * _Nullable info) {
// 该回调貌似不一定在主线程
if ([NSThread isMainThread])
completion(livePhoto, info);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(livePhoto, info);
});
}];
}
@end
终于搞定了,实属不易,宝宝心里苦…
- 获取视频的预览资源
视频资源直接获取AVAsset
即可,即便视频在iCloud上,没有关系,边播边缓冲,没什么好说的(终于遇到一个不是很坑的API了…)。而针对于requestPlayerItemForVideo
这个方法,如果视频在iCloud上,那么返回的AVPlayItem
为nil。
+ (void)requestAVAssetForAsset:(PHAsset *)asset completion:(void (^)(AVAsset * asset, AVAudioMix * audioMix, NSDictionary * info))completion
{
if (!completion)
return ;
PHVideoRequestOptions * option = [[PHVideoRequestOptions alloc] init];
option.version = PHVideoRequestOptionsVersionOriginal;
option.deliveryMode = PHVideoRequestOptionsDeliveryModeAutomatic;
option.networkAccessAllowed = YES;
[[PHImageManager defaultManager] requestAVAssetForVideo:asset options:option resultHandler:^(AVAsset * _Nullable asset, AVAudioMix * _Nullable audioMix, NSDictionary * _Nullable info) {
if ([NSThread isMainThread])
completion(asset, audioMix, info);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(asset, audioMix, info);
});
}];
}
到这里获取各种资源类型的预览资源都搞定了,很不易。
5.判断资源是否在本地
其实针对于图片资源来说,我们通过设置`为
NO,并且通过
requestImageDataForAsset:方法来获取
imageData,如果
imageData存在,那么说明在本地,如果
imageData为
nil说明在iCloud上。这个方法针对静态图、动态图和LIVE Photo都试用,虽然LIVE Photo本质上是
image + MOV,但是可以想象图片和视频一体的,如果图片有的话,那么视频也就有,所以判断LIVE Phot也可以通过这个方法判断。++判断是否在本地的方法较为耗时,建议异步处理。++
AVPlayItem
  针对于视频,就像上一条说的,我们获取视频的即可,如果为
nil`那么表示视频在iCloud上。
+ (void)judgeAssetIsInLocalAlbum:(PHAsset *)asset completion:(void (^)(BOOL isInLocalAlbum))completion
{
if (!completion)
return ;
if (asset.mediaType == PHAssetMediaTypeImage)
{
// 针对于LivePhoto来说,不用单独的去判断别的,因为如果有的话,image和mov都有,如果没有的话,都没有,如果下载的话,会把两个都下载下来
PHImageRequestOptions * option = [[PHImageRequestOptions alloc] init];
option.networkAccessAllowed = NO;
// 修改为异步,但不是全部都是异步,也有可能是同步,但是优化了很多
option.synchronous = NO;
[[PHCachingImageManager defaultManager] requestImageDataForAsset:asset options:option resultHandler:^(NSData * _Nullable imageData, NSString * _Nullable dataUTI, UIImageOrientation orientation, NSDictionary * _Nullable info) {
BOOL isInLocalAlbum = imageData ? YES : NO;
if ([NSThread isMainThread])
completion(isInLocalAlbum);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(isInLocalAlbum);
});
}];
}
else if (asset.mediaType == PHAssetMediaTypeVideo)
{
PHVideoRequestOptions * option = [[PHVideoRequestOptions alloc] init];
option.networkAccessAllowed = NO;
[[PHCachingImageManager defaultManager] requestPlayerItemForVideo:asset options:option resultHandler:^(AVPlayerItem * _Nullable playerItem, NSDictionary * _Nullable info) {
BOOL isInLocalAlbum = playerItem ? YES : NO;
if ([NSThread isMainThread])
completion(isInLocalAlbum);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(isInLocalAlbum);
});
}];
}
}
6.LIVE Photo导出
上面提到了LIVE Photo本质上是image + MOV
,所以如果导出的话,我们直接拿到视频资源和图片资源就好了。那么如果拿到视频资源呢?想想获取GIF的data,我们用到了PHAssetResourceManager
的writeDataForAssetResource:toFile:options:completionHandler:
方法,这里也同样试用,我们拿到资源后导出即可。
+ (void)exportLivePhotoForAsset:(PHAsset *)asset completion:(void (^)(NSString * exportFilePath, NSError * error))completion;
{
[self requestLivePhotoForAsset:asset completion:^(PHLivePhoto *livePhoto, NSDictionary *info) {
NSArray <PHAssetResource *> * assetResources = [PHAssetResource assetResourcesForLivePhoto:livePhoto];
BOOL find = NO;
for (PHAssetResource * assetResource in assetResources)
{
// 如果要拿图片资源,这个修改为PHAssetResourceTypePhoto即可
if (assetResource.type == PHAssetResourceTypeVideo || assetResource.type == PHAssetResourceTypePairedVideo)
{
NSString * exportFilePath = [self getExportFilePathForType:AssetMediaTypeLivePhoto];
PHAssetResourceRequestOptions * option = [[PHAssetResourceRequestOptions alloc] init];
option.networkAccessAllowed = YES;
[[PHAssetResourceManager defaultManager] writeDataForAssetResource:assetResource toFile:[NSURL fileURLWithPath:exportFilePath] options:option completionHandler:^(NSError * _Nullable error) {
if ([NSThread isMainThread])
completion(exportFilePath, error);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(exportFilePath, error);
});
}];
find = YES;
break;
}
}
if (find == NO)
{
if ([NSThread isMainThread])
completion(nil, [NSError errorWithDomain:@"com.kingsword.TuGeLe" code:-1 userInfo:nil]);
else
dispatch_async(dispatch_get_main_queue(), ^{
completion(nil, [NSError errorWithDomain:@"com.kingsword.TuGeLe" code:-1 userInfo:nil]);
});
}
}];
}
7.相册中资源类型的筛选
我们之前还有一个需求,就是在一个相册里面按照资源的类型筛选(是不是很变态)。开始没有思路,后来做的时候就比较清楚了,我们自己定义了资源类型:
typedef enum : NSUInteger {
AssetMediaTypeUnknown = 0,
// 单独静图
AssetMediaTypeStaticGraph = 1 << 0,
// 单独gif
AssetMediaTypeGif = 1 << 1,
// 照片(包含GIF,但不处理LIVE)
AssetMediaTypePhoto = AssetMediaTypeStaticGraph | AssetMediaTypeGif,
// 单独LIVE
AssetMediaTypeLivePhoto = 1 << 2,
// 照片以及LIVE照片
AssetMediaTypePhotoAndLive = AssetMediaTypePhoto | AssetMediaTypeLivePhoto,
// 视频
AssetMediaTypeVideo = 1 << 3,
// 照片和视频
AssetMediaTypePhotoAndVideo = AssetMediaTypePhoto | AssetMediaTypeVideo,
// 照片、视频、LIVE
AssetMediaTypeAll = AssetMediaTypePhoto | AssetMediaTypeVideo | AssetMediaTypeLivePhoto
} AssetMediaType;
那么如果我们要筛选什么样类型的图,我们通过这个枚举类型去获取就好了,无论是单种类型,还是多种类型,都不在话下。下面以所有照片这个相册为例。
+ (NSMutableArray <AlbumItem *> *)getItemsInAlbumList:(AlbumList *)albumList withType:(AssetMediaType)mediaType limit:(NSInteger)limit
{
NSMutableArray <AlbumItem *> * albumItems = [NSMutableArray array];
// 这个fetchResult里面的是这个相册下所有PHAsset的结果集
[albumList.fetchResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (albumItems.count == limit)
{
*stop = YES;
return ;
}
AssetMediaType type = [self getTypeForAsset:obj];
// 产品需求,如果LIVE Photo存在,那么他对应的静态图中也要存在与静态图筛选器中
if (type == AssetMediaTypeLivePhoto && (mediaType & AssetMediaTypeLivePhoto) == 0)
{
if (mediaType & AssetMediaTypeStaticGraph)
type = AssetMediaTypeStaticGraph;
else
return ;
}
else if ((mediaType & type) == 0)
return ;
NSString * duration = [self getDuration:obj];
[albumItems addObject:[AlbumItem itemWithAsset:obj type:type duration:duration]];
}];
return albumItems;
}
总结
自定义相册看起来容易,其实里面要挖掘,还是有很多坑的,这些只是针对于我们项目中的需求开发遇到的一些坑,有更深层次的自定义可能还会有很多其他坑。其实遇到坑也不要慌,搜搜资料,看看官方文档,都可以解决。最后原大家踏平所有坑,早日成为大牛…