iOS7、8、9相冊適配 – iPhone手機開發 iPhone軟體開發教學課程

前言

由於在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是個基類,它有兩個子類。分別是PHAssetCollectionPHCollectionListPHAssetCollection代表 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。

這種場景可以在數據層對ALAssetsGroupPHAssetCollection以及ALAssetPHAsset做抽象,抽取適配器即可。

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

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *