iOS封装相册API的tips

首先几乎每个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上,即便我们设置了networkAccessAllowedYES,也可能会存在返回的图片是静态图的情况++。百思不得解,最后也是没有办法的办法,通过PHAssetResourceManagerwriteDataForAssetResource: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存在,那么说明在本地,如果imageDatanil说明在iCloud上。这个方法针对静态图、动态图和LIVE Photo都试用,虽然LIVE Photo本质上是image + MOV,但是可以想象图片和视频一体的,如果图片有的话,那么视频也就有,所以判断LIVE Phot也可以通过这个方法判断。++判断是否在本地的方法较为耗时,建议异步处理。++
&emsp;&emsp;针对于视频,就像上一条说的,我们获取视频的
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,我们用到了PHAssetResourceManagerwriteDataForAssetResource: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;
}

总结

  自定义相册看起来容易,其实里面要挖掘,还是有很多坑的,这些只是针对于我们项目中的需求开发遇到的一些坑,有更深层次的自定义可能还会有很多其他坑。其实遇到坑也不要慌,搜搜资料,看看官方文档,都可以解决。最后原大家踏平所有坑,早日成为大牛…

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/80541889