相册适配
前言
由于在iOS8及以后苹果将原有的操作相册的ALAssetsLibrary framework替换为Photos framework,所以,如果在应用中使用到的相册需要支持iOS8以下的系统版本的话,就需要了解Photos framework以做不同的版本适配。
一、iOS8以下
1. 几个重要的实体概念
-
ALAsset(iOS8及以后使用PHAsset)
一个ALAsset
实例对象代表一个资源实体,比如一张图片、一个视频。共有三种类型:ALAssetTypePhoto // 图片 ALAssetTypeVideo // 视频 ALAssetTypeUnknown // 未知
通过这个实例,你可以获取到这个资源的创建时间、资源类型、缩略图、二进制数据等信息。
ALAssetsGroup(iOS8及以后使用PHAssetCollection)
一个ALAssetsGroup
实例对象代表一组资源的集合,也就是一个相册,可以是系统默认存在的相册(相机胶卷),也可以是开发者给用户创建的自定义相册(QQ)。
通过它,你可以获取到这个相册的名称、封面缩略图等。ALAssetsLibrary(iOS8及以后使用PHPhotoLibrary)
一个ALAssetsLibrary
实例对象对所有的相册资源进行索引。
使用它,你可以获取指定类型的相册、单张图片等对象,也可以添加新的自定义相册或者图片到相薄。如果你想要知道有没有获取到系统相薄的访问权限,同样需要用到它。
2. 实体相关API
ALAssetsLibrary
在iOS7及以下系统中,如果你想要获取相册中的资源,或者对相册进行操作,那么你先要创建一个ALAssetsLibrary实例对象。
-
获取指定类型的相册
- (void)enumerateGroupsWithTypes:(ALAssetsGroupType)types usingBlock:(ALAssetsLibraryGroupsEnumerationResultsBlock)enumerationBlock failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
注意:
由于这里的遍历都是异步的操作,所以ALAssetsLibrary的实例对象需要被一个静态变量或者成员变量引用来保证不被销毁,回调的Block才能保证被成功调用。下面同此处。
-
根据一个相册URL获取这个相册
- (void)groupForURL:(NSURL *)groupURL resultBlock:(ALAssetsLibraryGroupResultBlock)resultBlock failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
-
创建一个相册
- (void)addAssetsGroupAlbumWithName:(NSString *)name resultBlock:(ALAssetsLibraryGroupResultBlock)resultBlock failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
-
相册授权状态
+ (ALAuthorizationStatus)authorizationStatus
ALAssetsGroup
iOS7及以下系统的中每个相册都是一个ALAssetsGroup实例,你可以使用这个实例获取这个相册的相关信息。
-
获取相册信息
- (id)valueForProperty:(NSString *)property
参数property包含以下类型:
ALAssetsGroupPropertyName
ALAssetsGroupPropertyType
ALAssetsGroupPropertyPersistentID
ALAssetsGroupPropertyURL
-
封面图
- (CGImageRef)posterImage
-
相册包含实体(照片、视频)的数量
- (NSInteger)numberOfAssets
-
过滤规则
- (void)setAssetsFilter:(ALAssetsFilter *)filter
注意:
参数中的filter是一个ALAssetsFilter实例,这个实例只有三种类型:
+ (ALAssetsFilter *)allPhotos; // 所有图片
+ (ALAssetsFilter *)allVideos; // 所有视频
+ (ALAssetsFilter *)allAssets; // 所有视频及图片
在使用- (NSInteger)numberOfAssets
获取相册实体数量时,会依赖该过滤规则。比如一个相薄中存在5个视频和5张图片,如果指定allPhotos,则numberOfAssets返回值为5。同样使用- (void)enumerateAssetsUsingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock
获取相册中的所有资源时,也只能获取到指定过滤规则下的资源。
-
使用相应遍历规则获取相册中的所有ALAsset资源
- (void)enumerateAssetsWithOptions:(NSEnumerationOptions)options usingBlock:(ALAssetsGroupEnumerationResultsBlock)enumerationBlock
3. 使用示例
-
获取所有相册,并过滤相册中的视频
+ (void)getAssetsGroupsForIos8BelowSuccess:(void (^)(NSMutableArray *))success failure:(void (^)(NSError *error))failure { NSMutableArray *assetsGroups = [NSMutableArray array]; [[self library] enumerateGroupsWithTypes:ALAssetsGroupAll usingBlock:^(ALAssetsGroup *group, BOOL *stop) { if (group != nil) { [group setAssetsFilter:[ALAssetsFilter allPhotos]]; if (group.numberOfAssets > 0) { [assetsGroups addObject:group]; } } else { if (success) { success(assetsGroups); } } } failureBlock:^(NSError *error) { if (failure) { failure(error); } }];
}
- (ALAssetsLibrary *)library {
static ALAssetsLibrary *library;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
library = [[ALAssetsLibrary alloc] init];
});
return library;
}
-
获取一个相册中的所有资源
+ (void)getTimeLineSectionModelsForIos8BelowWithGroup:(MRAlbumGroupModel *)group success:(void (^)(NSMutableArray *))success failure:(void (^)(NSError *))failure { NSMutableArray *sectionModels = [NSMutableArray array]; [group.assetGroup enumerateAssetsWithOptions:NSEnumerationReverse usingBlock:^(ALAsset *result, NSUInteger index, BOOL *stop) { if(result) { [sectionModels addObject:result]; } }]; if (success != nil && sectionModels.count > 0) { success(sectionModels); } if (failure != nil && sectionModels.count == 0) { failure(nil); }
}
```
二、iOS8及以上
1. 几个重要的实体概念
与ALAssetsLibrary framework对应的几个实体
-
PHAsset
一个PHAsset
实例对象与ALAsset
类似,代表一个资源实体,比如一张图片、一个视频。与ALAsset
不同之处在于,多了一种实体类型,共四种类型:PHAssetMediaTypeUnknown = 0, PHAssetMediaTypeImage = 1, PHAssetMediaTypeVideo = 2, PHAssetMediaTypeAudio = 3,
同时,还多了更具体的子类型PHAssetMediaSubtype:
```
PHAssetMediaSubtypeNone = 0,
// Photo subtypes
PHAssetMediaSubtypePhotoPanorama = (1UL << 0),
PHAssetMediaSubtypePhotoHDR = (1UL << 1),
PHAssetMediaSubtypePhotoScreenshot PHOTOS_AVAILABLE_IOS_TVOS(9_0, 10_0) = (1UL << 2),
PHAssetMediaSubtypePhotoLive PHOTOS_AVAILABLE_IOS_TVOS(9_1, 10_0) = (1UL << 3),
// Video subtypes
PHAssetMediaSubtypeVideoStreamed = (1UL << 16),
PHAssetMediaSubtypeVideoHighFrameRate = (1UL << 17),
PHAssetMediaSubtypeVideoTimelapse = (1UL << 18),
```
通过这个实例,除了可以获取到这个资源的创建时间、资源类型、缩略图、二进制数据等信息,还可以获取到location等位置信息。
-
PHCollection
PHCollection
是个基类,它有两个子类。分别是PHAssetCollection
和PHCollectionList
,PHAssetCollection
代表 Photos 中的相册,PHCollectionList
代表 Photos 中的文件夹。PHCollectionList
里可嵌套PHAssetCollection
,也可以嵌套自身类型,同时支持多重嵌套。
一个PHAssetCollection
实例对象与ALAssetsGroup
类似,代表一组资源的集合,也就是一个相册。
通过它,你可以获取到这个相册的名称、封面缩略图。
以及相册类型PHAssetCollectionType
:PHAssetCollectionTypeAlbum = 1, // 自定义相册,如QQ PHAssetCollectionTypeSmartAlbum = 2, // 相机胶卷、我的照片流、屏幕截图、全景照片等 PHAssetCollectionTypeMoment = 3, // 时刻
相册子类型
PHAssetCollectionSubtype
:// PHAssetCollectionTypeAlbum regular subtypes PHAssetCollectionSubtypeAlbumRegular = 2, PHAssetCollectionSubtypeAlbumSyncedEvent = 3, PHAssetCollectionSubtypeAlbumSyncedFaces = 4, PHAssetCollectionSubtypeAlbumSyncedAlbum = 5, PHAssetCollectionSubtypeAlbumImported = 6, // PHAssetCollectionTypeAlbum shared subtypes PHAssetCollectionSubtypeAlbumMyPhotoStream = 100, PHAssetCollectionSubtypeAlbumCloudShared = 101, // PHAssetCollectionTypeSmartAlbum subtypes PHAssetCollectionSubtypeSmartAlbumGeneric = 200, PHAssetCollectionSubtypeSmartAlbumPanoramas = 201, PHAssetCollectionSubtypeSmartAlbumVideos = 202, PHAssetCollectionSubtypeSmartAlbumFavorites = 203, PHAssetCollectionSubtypeSmartAlbumTimelapses = 204, PHAssetCollectionSubtypeSmartAlbumAllHidden = 205, PHAssetCollectionSubtypeSmartAlbumRecentlyAdded = 206, PHAssetCollectionSubtypeSmartAlbumBursts = 207, PHAssetCollectionSubtypeSmartAlbumSlomoVideos = 208, PHAssetCollectionSubtypeSmartAlbumUserLibrary = 209, PHAssetCollectionSubtypeSmartAlbumSelfPortraits PHOTOS_AVAILABLE_IOS_TVOS(9_0, 10_0) = 210, PHAssetCollectionSubtypeSmartAlbumScreenshots PHOTOS_AVAILABLE_IOS_TVOS(9_0, 10_0) = 211, // Used for fetching, if you don't care about the exact subtype PHAssetCollectionSubtypeAny = NSIntegerMax
PHPhotoLibrary
PHPhotoLibrary
是个单例对象,与ALAssetsLibrary
相比发生了较大变化,只能申请获取PHOtos权限以及授权状态获取等。
使用这个单例你也可以注册相册改动的监听者,在相册发生变化(比如添加或导入了新图片)时,做一些刷新或其他处理。
新的实体
在iOS8及以上的相册资源获取中,都是使用fetch
相关API的形式,过程类似Core Data
。匹配资源的过程自然需要匹配规则,最终匹配结果也需要一个集合来记录。
PHFetchOptions
PHFetchOptions
的实例对象代表获取资源时的匹配规则。
可以指定资源排序规则(NSArray
)、资源类型规则(*sortDescriptors; NSPredicate *predicate
)、最大数量(NSUInteger fetchLimit
)等。PHFetchResult
PHFetchResult
的实例对象代表相册资源的匹配结果集合。它包含零个或多个符合匹配规则的资源,这些资源可以是PHAssetCollection
对象,也可以是PHAsset
对象。PHImageManager
PHImageManager
是一个单例对象,不需要你手动创建,这个单例对象可以让你获取到一个PHAsset资源的实际二进制数据——如一张图片数据。
2. 实体相关API
PHAssetCollection
在iOS8及以上系统中,获取相册不需要创建实例,直接使用PHAssetCollection
的类方法进行操作即可。
-
获取指定类型的相册
// Fetch asset collections of a single type and subtype provided (use PHAssetCollectionSubtypeAny to match all subtypes) + (PHFetchResult
*)fetchAssetCollectionsWithType:(PHAssetCollectionType)type subtype:(PHAssetCollectionSubtype)subtype options:(nullable PHFetchOptions *)options; -
基本属性
@property (nonatomic, strong, readonly, nullable) NSString *localizedTitle; // 相册标题 @property (nonatomic, assign, readonly) PHAssetCollectionType assetCollectionType; // 相册类型 @property (nonatomic, assign, readonly) PHAssetCollectionSubtype assetCollectionSubtype; // 子类型 @property (nonatomic, assign, readonly) NSUInteger estimatedAssetCount; // 预估资源数量 @property (nonatomic, strong, readonly, nullable) NSDate *startDate; // 开始日期 @property (nonatomic, strong, readonly, nullable) NSDate *endDate; // 结束日期 @property (nonatomic, strong, readonly, nullable) CLLocation *approximateLocation; // 位置信息 @property (nonatomic, strong, readonly) NSArray
*localizedLocationNames; // 位置名称
注意:
estimatedAssetCount
并不是一个准确的值,当PHAssetCollection
对象不能马上计算出当前相册中的资源数量时,会返回NSNotFound
,如果想要获取资源的具体数量,需要使用PHAsset
来fetch所有资源,计算资源数量。
PHAsset
-
获取某相册中的PHAsset资源
+ (PHFetchResult
*)fetchAssetsInAssetCollection:(PHAssetCollection *)assetCollection options:(nullable PHFetchOptions *)options; -
获取某相册的封面资源
+ (nullable PHFetchResult
*)fetchKeyAssetsInAssetCollection:(PHAssetCollection *)assetCollection options:(nullable PHFetchOptions *)options;
注意:
这个API可以获取到系统默认的一些封面图资源,这个结果由零个或多个PHAsset组成,在相册是smartAlbum类型的情况下,可能会有零个结果的情况。
-
获取中Photos中的所有PHAsset资源
+ (PHFetchResult
*)fetchAssetsWithOptions:(nullable PHFetchOptions *)options;
注意:
这里并不是获取某个相册中的PHAsset资源,而是手机中所有的PHAsset资源。
比如手机中有10个相册,每个相册中10个PHAsset资源,如果不指定PHFetchOptions,使用该方法会获取到所有的100个PHAsset资源。
-
获取Photos中指定资源类型的所有PHAsset资源
+ (PHFetchResult
*)fetchAssetsWithMediaType:(PHAssetMediaType)mediaType options:(nullable PHFetchOptions *)options;
参数中的mediaType可选:
```
PHAssetMediaTypeUnknown = 0,
PHAssetMediaTypeImage = 1,
PHAssetMediaTypeVideo = 2,
PHAssetMediaTypeAudio = 3,
```
PHImageManager
-
获取PHAsset图片资源的图片数据
- (PHImageRequestID)requestImageForAsset:(PHAsset *)asset targetSize:(CGSize)targetSize contentMode:(PHImageContentMode)contentMode options:(nullable PHImageRequestOptions *)options resultHandler:(void (^)(UIImage *__nullable result, NSDictionary *__nullable info))resultHandler;
注意:
参数中的PHImageRequestOptions
对象可以设置目标图片的一些属性,其中synchronous表示是否同步获取,默认是NO,也就是异步获取。
3. 使用示例
-
获取指定类型的相册
+ (NSMutableArray *)getCollecionsWithSmartAlbumSubtype:(PHAssetCollectionSubtype)subtype { PHFetchOptions *userAlbumsOptions = [PHFetchOptions new]; PHFetchResult *userAlbumsResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAny options:userAlbumsOptions]; PHFetchResult *userSmartAlbumsResult = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:subtype options:userAlbumsOptions]; NSMutableArray *collections = [NSMutableArray array]; void (^AlbumEnumerateObjectsUsingBlock)(PHAssetCollection *, NSUInteger idx, BOOL *) = ^(PHAssetCollection * _Nonnull collection, NSUInteger idx, BOOL * _Nonnull stop) { if (collection.estimatedAssetCount == 0) { return ; } NSUInteger numberOfAssets = 0; PHFetchResult *assetsResult = [PHAsset fetchAssetsInAssetCollection:collection options:nil]; numberOfAssets = [assetsResult countOfAssetsWithMediaType:PHAssetMediaTypeImage]; if (numberOfAssets == 0) { return; } [collections addObject:collection]; }; [userSmartAlbumsResult enumerateObjectsUsingBlock:AlbumEnumerateObjectsUsingBlock]; [userAlbumsResult enumerateObjectsUsingBlock:AlbumEnumerateObjectsUsingBlock]; return collections;
}
```
-
获取一个相册中的所有图片资源,按创建时间降序排序
+ (void)getTimeLineSectionModelsForIos8AboveWithGroup:(MRAlbumGroupModel *)group success:(void (^)(NSMutableArray *))success failure:(void (^)(NSError *))failure { PHFetchResult *assetsResult = [PHAsset fetchAssetsInAssetCollection:group.collection options:[self fetchOptions]]; NSMutableArray *sectionModels = [NSMutableArray array]; [assetsResult enumerateObjectsUsingBlock:^(PHAsset * _Nonnull asset, NSUInteger idx, BOOL * _Nonnull stop) { [sectionModels addObject:asset]; }]; if (success != nil && sectionModels.count > 0) { success(sectionModels); } if (failure != nil && sectionModels.count == 0) { failure(nil); }
}
- (PHFetchOptions *)fetchOptions {
PHFetchOptions *fetchOptions = [PHFetchOptions new];
fetchOptions.predicate = [NSPredicate predicateWithFormat:@"mediaType = %@", @(PHAssetMediaTypeImage)];
fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
return fetchOptions;
}
三、适配参考
我在不同系统的相册适配中采用适配器的方式进行的适配,仅供参考。
1. 适配器
相册适配主要是为了解决在iOS8以上的系统中,使用ALLibrary相关API可能存在一些不兼容的问题,导致资源获取不一致情况的出现。
在业务展现上并不期望因系统不同而给用户差别较大的体验,所以可以只针对相册资源数据获取的数据测适配即可,而无需改动UI。
这种场景可以在数据层对ALAssetsGroup
和PHAssetCollection
以及ALAsset
和PHAsset
做抽象,抽取适配器即可。
2. 简单示例
-
AssetGroupModel
@interface AlbumGroupModel : NSObject @property (nonatomic, strong) UIImage *posterImage; @property (nonatomic, assign) NSUInteger numberOfAssets; @property (nonatomic, copy) NSString *assetsGroupPropertyName; @property (nonatomic, strong) PHAssetCollection *collection; @property (nonatomic, strong) ALAssetsGroup *assetGroup; @end @implementation AlbumGroupModel
-
(UIImage *)posterImage {
if (_posterImage == nil) {
if ([XSLAppInfoTool systemVersion] < 8.0) {
_posterImage = [UIImage imageWithCGImage:self.assetGroup.posterImage];
} else {
PHFetchResult *groupResult = [PHAsset fetchAssetsInAssetCollection:self.collection options:self.class.fetchOptions];
PHAsset *asset = groupResult.firstObject;
PHImageRequestOptions *requestOptions = [[PHImageRequestOptions alloc] init];
requestOptions.resizeMode = PHImageRequestOptionsResizeModeExact;CGFloat scale = [UIScreen mainScreen].scale; CGFloat dimension = 80.0f; CGSize size = CGSizeMake(dimension * scale, dimension * scale); __block UIImage *resultImage = nil; [[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeAspectFill options:requestOptions resultHandler:^(UIImage *result, NSDictionary *info) { resultImage = result; }]; _posterImage = resultImage; }
}
return _posterImage;
}
@end -
AssetModel
@interface AlbumAssetModel : NSObject @property (nonatomic, strong, nullable) PHAsset *assetPh; @property (nonatomic, strong, nullable) ALAsset *assetAl; - (void)thumbnail:(nullable void(^)(UIImage * _Nullable thumbnail))resultCallback; @end @implementation AlbumAssetModel - (void)thumbnail:(void (^)(UIImage *))resultCallback { if (self.assetPh) { PHImageManager *manager = [PHImageManager defaultManager]; [manager requestImageForAsset:self.assetPh targetSize:CGSizeMake(100, 100) contentMode:PHImageContentModeDefault options:nil resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) { if (resultCallback) { resultCallback(result); } }]; } else if (self.assetAl) { if (resultCallback) { resultCallback([UIImage imageWithCGImage:[self.assetAl thumbnail]]); } } } @end