在 iOS 中,每个相册资源,包括图片,视频,GIF 图片都是一个PHAsset
对象来表示,自定义相册的时候,实际上就是获取到每个相册资源PHAsset
,然后再显示的时候通过PHAsset
获取对应的视频,图片或者 GIF 显示,相册一般使用UICollectionView
来展示,利用UICollectionView
的 cell 复用来有效提高效率,在滚动的时候动态的获取图片资源加载,减少内存占用和卡顿
一. PhotoKit 常用类
-
PHAsset
:常用属性
// mediaType:表明资源类型
typedef NS_ENUM(NSInteger, PHAssetMediaType) {
PHAssetMediaTypeUnknown = 0,
PHAssetMediaTypeImage = 1,//图片
PHAssetMediaTypeVideo = 2,//视频
PHAssetMediaTypeAudio = 3,//音频
} PHOTOS_ENUM_AVAILABLE_IOS_TVOS(8_0, 10_0);
//资源像素宽/高
@property (nonatomic, assign, readonly) NSUInteger pixelWidth;
@property (nonatomic, assign, readonly) NSUInteger pixelHeight;
//视频时长
@property (nonatomic, assign, readonly) NSTimeInterval duration;
-
PHFetchResult
: 访问获取的结果(按需从备份存储区中获取对象,而不是一次全部获取,获取的对象将保存在缓存中,并在内存压力下清除.无论我们是获取相册,还是具体视频图片资源,都是返回一个PHFetchResult
,具体资源需要从PHFetchResult
里获取
二. PhotoKit 的使用
- 权限获取:
plist 文件权限配置
<key>NSCameraUsageDescription</key>
<string>将用于采集拍照和视频拍摄</string>
<key>NSMicrophoneUsageDescription</key>
<string>将用于录制视频音频采集和发送语言消息</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>保存图片或视频到本地相册</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>将用于头像设置,视频图片文件上传</string>
获取相册资源的时候,需要确定用户授权状态,状态异步返回,第一次会有弹窗提示,在得到用户允许之后获取相册资源
- (void)requestAuthorityhandle:(void(^)(BOOL allow))handle{
if ([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusAuthorized){//用户之前已经授权
handle(true);
}else if([PHPhotoLibrary authorizationStatus] == PHAuthorizationStatusDenied){//用户之前已经拒绝授权
handle(NO);
}else{//弹窗授权时监听
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
if (status == PHAuthorizationStatusAuthorized){//允许
handle(true);
}else{//拒绝
handle(NO);
}
}];
}
}
- 自定义相册/资源对象
@interface AlbumAssetModel : NSObject
@property (strong, nonatomic) PHAssetCollection * albumAsset;//相册资源
@property (copy, nonatomic) NSArray<ImageAssetModel*> * imageModels;//当前相册包含的资源
@property (strong, nonatomic) NSString * albumID;//相册 ID
@property (strong, nonatomic) NSString * albumName;//相册名字
@property (strong, nonatomic) UIImage * albumImage;//相册封面图片
@property (assign, nonatomic) NSInteger count;//相册包含的资源数
@property (assign, nonatomic, getter=isSelected) BOOL selected;//当前相册是否被选中
//-------------- 每个相册资源对象
typedef NS_ENUM(NSInteger,ImageAssetModelType){
ImageAssetModelTypePhoto,
ImageAssetModelTypeVideo,
ImageAssetModelTypLivePhoto,
ImageAssetModelTypePhotoGIf
};//定义当前资源的类型
@interface ImageAssetModel : NSObject
@property (strong, nonatomic) UIImage * smallmage;//缩略图
@property (strong, nonatomic) NSString * imageAssetName;//资源名称
@property (strong, nonatomic) NSString * photoPath;//资源路径
@property (strong, nonatomic) UIImage * hightImage;//高清图
@property (strong, nonatomic) UIImage * originImage;//原图
@property (strong, nonatomic) PHAsset * imageAsset;//相册资源
@property (strong, nonatomic) NSString * imageID;
@property (assign, nonatomic) CGSize imageSize; //内存大小
@property (assign, nonatomic, getter=isSelected) BOOL selected;
@property (assign, nonatomic) ImageAssetModelType assetType;
- 获取相册:
- (void)getSmartAlbumsWidthType:(requestImageType)type {
self.requesType = type;
NSMutableArray * albumArray = [NSMutableArray array];
//获取相册总资源
PHFetchResult * albumRestle = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeSmartAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil ];
//每个相册资源是一个PHAssetCollection
for (PHAssetCollection * collection in albumRestle) {
// 有可能是PHCollectionList类的的对象,过滤掉
if (![collection isKindOfClass:[PHAssetCollection class]]) continue;
//过滤空相册
if (collection.estimatedAssetCount <= 0) continue;
//PHAssetCollection 得到自定义相册对象
AlbumAssetModel * model = [self modelWithCollection:collection];
if(model.imageModels.count > 0){
[albumArray addObject:model];
}
}
//把相册按包含的资源数量排序
NSArray *resultArray = [albumArray sortedArrayUsingComparator:^NSComparisonResult(AlbumAssetModel * obj1, AlbumAssetModel * obj2) {
NSNumber * number1 = [NSNumber numberWithInteger:obj1.imageModels.count];
NSNumber * number2 = [NSNumber numberWithInteger:obj2.imageModels.count];
NSComparisonResult result = [number1 compare:number2];
// return result == NSOrderedDescending; // 升序
return result == NSOrderedAscending; // 降序
}];
}
//返回相册模型
- (void)modelWithCollection:(PHAssetCollection *)collection{
//资源访问配置
PHFetchOptions * options = [self configRequestOptions];
//获取相册里的资源
PHFetchResult *fetchResult = [PHAsset fetchAssetsInAssetCollection:collection options:options];
AlbumAssetModel *model = [[AlbumAssetModel alloc]init];
model.albumName = collection.localizedTitle;
model.albumID = collection.localIdentifier;
model.albumAsset = collection;
model.count = fetchResult.count;
model.selected = NO;
//得到相册资源模型
model.imageModels = [self getAssetsFromFetchResult:fetchResult];
[self getAlbumImageWithAssetModel:model.imageModels.firstObject handler:^(BOOL success, UIImage *image) {
if (success) {//获取封面图片
model.albumImage = image;
}
}];
}
//配置资源获取的过滤条件
- (PHFetchOptions *)configRequestOptions {
PHFetchOptions * options = [[PHFetchOptions alloc]init];
if(self.requesType == requestImageTypeImage) {
options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d",PHAssetMediaTypeImage];
}else if(self.requesType == requestImageTypeVideo){
options.predicate = [NSPredicate predicateWithFormat:@"mediaType = %d",PHAssetMediaTypeVideo];
}
//资源按创建资源排序
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:self.sortRequestImage]];
return options;
}
//获取相册封面 指定宽高
- (void)getAlbumImageWithAssetModel:(ImageAssetModel*)model handler:(void(^)(BOOL success ,UIImage * image))handler{
[self getImageWithAsset:model photoWidth: 80.0 handler:^(BOOL success, UIImage *image) {
if (success) {
handler(true,image);
}
}];
}
- 获取图片资源
//获取图片数据//获取图片图片类型
- (NSArray*)getAssetsFromFetchResult:(PHFetchResult *)result{
NSMutableArray * photoArr = [NSMutableArray array];
[result enumerateObjectsUsingBlock:^(PHAsset *asset, NSUInteger idx, BOOL * _Nonnull stop) {
if ([self fetchSizeWithAsset:asset]) {
ImageAssetModel *model = [[ImageAssetModel alloc]init];
if (asset.mediaType == PHAssetMediaTypeVideo){
model.assetType = ImageAssetModelTypeVideo;
}else if(asset.mediaType == PHAssetMediaTypeImage) {
if ([[asset valueForKey:@"filename"] hasSuffix:@"GIF"]) {
model.assetType = ImageAssetModelTypePhotoGIf;
}else{
model.assetType = ImageAssetModelTypePhoto;
}
}
model.imageAssetName = [asset valueForKey:@"filename"];
model.imageAsset = asset;
model.imageID = asset.localIdentifier;
model.imageSize = CGSizeMake(asset.pixelWidth, asset.pixelHeight);
[photoArr addObject:model];
}
}];
return photoArr;
}
//获取图片
- (void)getImageWithAsset:(ImageAssetModel *)model photoWidth:(CGFloat)width handler:(void(^)(BOOL success ,UIImage * image))handler {
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
//options.resizeMode = PHImageRequestOptionsResizeModeFast; //fast 会导致稍微模糊
options.networkAccessAllowed = YES;
options.synchronous = NO;
if (model.assetType == ImageAssetModelTypePhotoGIf) {
options.version = PHImageRequestOptionsVersionOriginal;
}
PHAsset * phAsset = model.imageAsset;
CGFloat aspectRatio = phAsset.pixelWidth / (CGFloat)phAsset.pixelHeight;
CGFloat pixeWidth = width * screenScale;
//超宽图片
if (aspectRatio > 1.8) {
pixeWidth = pixeWidth * aspectRatio;
}
//超高图片
if(aspectRatio < 0.2){
pixeWidth = pixeWidth * 0.5;
}
CGFloat pixeHeight = pixeWidth / aspectRatio;
CGSize imageSize = CGSizeMake(pixeWidth, pixeHeight);
__block UIImage * image;
[[PHImageManager defaultManager] requestImageForAsset:phAsset targetSize:imageSize contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
if (result) {
handler(true,result);
}
}];
}
//获取原图
- (void)getOringinPhototWithAssetModel:(ImageAssetModel*)model handler:(nonnull void (^)(BOOL, UIImage * _Nonnull))handler{
__block UIImage * image;
PHImageRequestOptions *option = [[PHImageRequestOptions alloc]init];
option.resizeMode = PHImageRequestOptionsResizeModeFast;
option.synchronous = YES;
PHAsset * phAsset = model.imageAsset;
[[PHImageManager defaultManager] requestImageForAsset:phAsset targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeDefault options:option resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
image = [self fixOrientation:result];
if (image) {
handler(true,image);
}
}];
}
//获取图片NSData数据
- (void)getOriginImageDataWithAsset:(ImageAssetModel *)model handle:(void(^)(NSData * imagedata, bool success))handle{
PHImageRequestOptions *options = [[PHImageRequestOptions alloc] init];
options.resizeMode = PHImageRequestOptionsResizeModeFast;
options.networkAccessAllowed = YES;
if (model.assetType == ImageAssetModelTypePhotoGIf) {
options.version = PHImageRequestOptionsVersionOriginal;
}
[[PHImageManager defaultManager] requestImageDataForAsset:model.imageAsset options:options resultHandler:^(NSData *imageData, NSString *dataUTI, UIImageOrientation orientation, NSDictionary *info) {
if (imageData) {
handle(imageData, true);
}else {
handle(nil, NO);
}
}];
}
至此 相册资源的获取已经实现,我们已经得到相册资源,下面就是资源的展示
注意
:相册,图片,视频资源的返回都是异步的,需要等待异步结果返回再进行刷新 UI
//相册展示,使用tableView
展示相册
- (void)setModel:(AlbumAssetModel *)model {
self.albunImageV.image = model.albumImage;
self.albumNameLabel.text = model.albumName;
self.albumSelectedImageV.hidden = !model.isSelected;
}
- (void)setupSubViews{
[self.contentView addSubview:self.albunImageV];
[self.contentView addSubview:self.albumNameLabel];
[self.contentView addSubview:self.albumSelectedImageV];
[self.albunImageV mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.equalTo(self.contentView).offset(15);;
make.width.height.equalTo(@60);
//使用 tableView 展示的时候,需要 cell 子控件把整个 Cell 撑起来,所以需要指定上下边距和宽高,同时指定宽高和上下边距导致约束冲突,所以降低其中冲突约束的级别
make.bottom.equalTo(self.contentView).offset(-10).priority(500);
}];
[self.albumSelectedImageV mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.equalTo(self.contentView).offset(-15);
make.centerY.equalTo(self.albunImageV);
make.height.width.equalTo(@15);
}];
[self.albumNameLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.albunImageV.mas_right).offset(15);
make.centerY.equalTo(self.albumSelectedImageV);
make.height.equalTo(@20);
}];
}
- (UITableView *)albumtableView {
if (!_albumtableView) {
CGFloat tableViewHeight = kSCREEN_HEIGHT / 5 * 4;
_albumtableView = [[UITableView alloc]initWithFrame:CGRectMake(0, kSCREEN_HEIGHT - tableViewHeight/2 - 60 - kHEIGHT_BOTTOMSAFE + 5, kSCREEN_WIDTH, tableViewHeight) style:UITableViewStylePlain];
_albumtableView.backgroundColor = [UIColor whiteColor];
_albumtableView.delegate = self;
_albumtableView.dataSource = self;
_albumtableView.rowHeight = UITableViewAutomaticDimension;
_albumtableView.estimatedRowHeight = 44;
_albumtableView.layer.anchorPoint = CGPointMake(0.5, 1);
_albumtableView.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 0.001);
[_albumtableView registerClass:[AlbumTableViewCell class] forCellReuseIdentifier:@"albumCell"];
}
return _albumtableView;
}
//点击选择相册
- (void)clickAlbumButton:(UIButton *)sender {
UIWindow * window = [UIApplication sharedApplication].keyWindow;
[window addSubview:self.albumBgView];
[self.albumtableView reloadData];
[UIView animateWithDuration:0.5 animations:^{
self.albumtableView.transform = CGAffineTransformScale(CGAffineTransformIdentity, 1, 1);
}];
}
- 相册资源展示:相册使用
UICollectionView
来展示
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
PhotoIteanCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"collectionCell" forIndexPath:indexPath];
//第一张图片如果需要显示拍照按钮的时候,点击第一个 cell,打开相机
if (self.isShowCamera) {
if (indexPath.row == 0) {
cell.type = PhotoIteamCellTypeCammer;
}else{
ImageAssetModel * model = self.assets[indexPath.row - 1];
cell.type = PhotoIteamCellTypePhoto;
[cell setImageWithImageAssetModel:model];
}
}else{
//点击右上角选中按钮的时候,对应资源为选中状态
ImageAssetModel * model = self.assets[indexPath.row];
cell.type = PhotoIteamCellTypePhoto;
[cell setImageWithImageAssetModel:model];
}
return cell;
}
//iteamCell
- (void)setImageWithImageAssetModel:(ImageAssetModel *)model {
self.imageModel = model;
self.selectedBtn.selected = model.isSelected;
[[ImageManager shareManager] getImageWithAsset:model handler:^(BOOL success, UIImage * _Nonnull image) {
if (success) {
self.imageView.image = image;
self.assetType = model.assetType;
}
}];
}
- 资源预览:使用
UICollectionView
预览资源,根据传进来的资源个数创建对应个数的 iteamCell,根据各个资源类型不同显示
//图片显示
[scrollV addSubview:imageV];
[cell.contentView addSubview:scrollV];
__block UIImage * assetImage = model.originImage;
if (!assetImage){
//预览的时候获取原图显示
[[ImageManager shareManager] getOringinPhototWithAssetModel:model handler:^(BOOL success, UIImage * _Nonnull image) {
if (success) {
assetImage = image;
}
}];
}
//获取图片宽高,如果是长图或者宽图的时候,重新设置UIScrollView来显示
CGFloat width = assetImage.size.width;
CGFloat height = assetImage.size.height;
float heightRate = height / width;
dispatch_async(dispatch_get_main_queue(), ^{
scrollV.contentSize = CGSizeMake(kSCREEN_WIDTH - 10, kSCREEN_WIDTH * heightRate);
imageV.frame = CGRectMake(0, 0, kSCREEN_WIDTH - 10, kSCREEN_WIDTH * heightRate);
if (heightRate * kSCREEN_WIDTH < kSCREEN_HEIGHT - kHEIGHT_TOPBAR) {
imageV.center = scrollV.center;
}
[cell layoutIfNeeded];
imageV.backgroundColor = [UIColor redColor];;
imageV.image = assetImage;
});
- gif 预览
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[ImageManager shareManager] getOriginImageDataWithAsset:model handle:^(NSData * _Nonnull imagedata, bool success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
FLAnimatedImageView *imgView = [FLAnimatedImageView new];
imgView.contentMode = UIViewContentModeScaleAspectFit;
imgView.frame = CGRectMake(30, 20, kSCREEN_WIDTH , kSCREEN_HEIGHT / 2);
imgView.backgroundColor = [UIColor clearColor];
imgView.center = cell.contentView.center;
imgView.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imagedata];
[cell.contentView addSubview:imgView];
});
- 视频预览
[[ImageManager shareManager] getVideoPathWithAssetModel:model handler:^(BOOL success, NSString * _Nonnull videoPath) {
if (success) {
AVURLAsset * asset = [AVURLAsset assetWithURL:[NSURL fileURLWithPath:videoPath]];
AVPlayerItem * avIteam = [[AVPlayerItem alloc]initWithAsset:asset];
self.player = [AVPlayer playerWithPlayerItem:avIteam];
AVPlayerLayer * layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
dispatch_async(dispatch_get_main_queue(), ^{
layer.frame = CGRectMake(0, 0, kSCREEN_WIDTH- 10, kSCREEN_HEIGHT - kHEIGHT_BOTTOMSAFE - kHEIGHT_TOPBAR);
layer.position = cell.contentView.center;
layer.contentsGravity = kCAGravityResizeAspect;
[cell.contentView.layer addSublayer:layer];
[cell layoutIfNeeded];
[self.player play];
});
}
}];