Photos 框架實踐以及坑

導讀

對于 Photos 框架的介紹,推薦觀看 objccn.io 的文章。寫得真好,我寫得的文章水準還差得老遠啊。本文總結了近期使用 Photos 框架編寫一個相冊的經驗,目前還有很大一部分的框架內容沒有涉及到,后續會更新內容。

獲取資源

照片庫中有兩種資源可供獲取:PHAssetPHCollection,前者代表圖像或視頻對象,后者是前者的集合或自身類型的集合。PHCollection是個基類,有PHAssetCollectionPHCollectionList兩個子類,分別代表 Photos 里的相冊和文件夾。以往使用 Photos 時,并沒有注意到可以建立文件夾,似乎是從 Photos 框架才支持這個功能,而PHCollectionList里可嵌套PHAssetCollection和自身類型,還支持多重嵌套。獲取PHAsset以及PHAssetCollection的過程類似于 Core Data,如下所示,只能通過類方法來返回PHFetchResult,遍歷返回的結果來獲取需要的資源。

PHAsset Fetch Method

PHAssetCollection Fetch Method

注意,PHAssetPHAssetCollectionPHCollectionList 都是輕量級的不可變對象,使用這些類時并沒有將其代表的圖像或視頻或是集合載入內存中,要使用其代表的圖像或視頻,需要通過PHImageManager類來請求。

請求圖像(這里有巨坑)

關于PHImageManager類,NSHipster 有篇總結文章不錯。

- requestImageForAsset:targetSize:contentMode:options:resultHandler:

你不應該生成該類的實例,而應該使用該類的提供的單例對象。該方法提供指定的尺寸的圖像,與ALAssetsLibrary庫相比,沒有了方便的縮略圖提供。不過要吐槽的是,ALAssetsLibrary庫提供的縮略圖往往尺寸太小并且質量很低,用在 TableView 上還可以。

需要注意的是,該方法在默認情況下是異步執行的,而且 Photos 庫可能會多次執行 resultHandler 塊,因為對于指定的尺寸,Photos 可能會先提供低質量的圖像以供臨時顯示,隨后會將指定尺寸的圖像返回。如果指定尺寸的高質量的圖像有緩存,那么直接提供高質量的圖像。而這些行為,可以通過 options 參數來定制。

PHImageRequestOptions類用于定制請求。這里有巨坑。上面的方法返回指定尺寸的圖像,如果你僅僅指定必要的參數而沒有對 options 進行配置的話,返回的圖像尺寸將會是原始圖像的尺寸。或者,你指定的尺寸很小,這時候會按照你的要求來返回接近該尺寸的圖像。在我的 iPad mini 一代上,對于自拍的圖像,指定尺寸不超過(257, 257)的話,返回的圖像尺寸和你預期的一樣,其他情況下都是原始尺寸。PHImageRequestOptions有以下幾個重要的屬性:

synchronous:指定請求是否同步執行。
resizeMode:對請求的圖像怎樣縮放。有三種選擇:None,不縮放;Fast,盡快地提供接近或稍微大于要求的尺寸;Exact,精準提供要求的尺寸。
deliveryMode:圖像質量。有三種值:Opportunistic,在速度與質量中均衡;HighQualityFormat,不管花費多長時間,提供高質量圖像;FastFormat,以最快速度提供好的質量。
             這個屬性只有在 synchronous 為 true 時有效。
normalizedCropRect:用于對原始尺寸的圖像進行裁剪,基于比例坐標。只在 resizeMode 為 Exact 時有效。

resizeMode 默認是 None,這也造成了返回圖像尺寸與要求尺寸不符。這點需要注意。要返回一個指定尺寸的圖像需要避免兩層陷阱:一定要指定 options 參數,resizeMode 不能為 None。

除了必有的請求圖像或是視頻的功能外,PHImageManager添加了兩大功能:
1.緩存圖像,由其子類PHCachingImageManager實現,緩存效率和空間管理能滿足大部分場景的需求;
2.裁剪圖像,這個功能很久以前就有強烈的需求。六年前 StackOverflow 上 Cropping a UIImage 這個問題就被提出來了,方法也五花八門,然而這些方法可能會有各種小問題。官方的方法能讓你避免這些小問題。使用方法可以參考 NSHipster 的總結文章里用人臉識別獲取頭像的例子。

localIdentifier vs URL

Photos 框架推出時,和原來的照片庫 AssetsLibrary 框架之間還有些交互,PHAsset 類的+ fetchAssetsWithALAssetURLs:options:PHAssetCollection類的 + fetchAssetCollectionsWithALAssetGroupURLs:options:可以利用原來的 AssetsLibrary 提供的 URL 進行轉化,而在 iOS 9 中,原來的照片框架 AssetsLibrary 已經被廢棄了,如今這兩個方法也沒有用處了。當初我還找過如何從 Photos 框架到 AssetsLibrary 框架的方法,理所當然地白費功夫,官方要淡化照片庫中 URL 的概念,改之使用一個標志符來唯一代表一個資源。Photos 框架中的根類PHObject只有一個公開接口localIdentifier,AssetsLibrary 框架中無論是 Asset 還是 AssetGroup 的 URL 也是唯一標志符,而且同時還是動態變化的,每次啟動應用后獲取的 URL 和上一次是不一樣的,而 AssetGroup 有一個 PersistentID 與PHObjectlocalIdentifier類似,但獲取比較麻煩。
localIdentifier屬性帶來的最大好處是PHObject類實現了 NSCopying 協議,可以直接使用localIdentifier屬性對PHObject及其子類對象進行對比是否同一個對象。

獲取指定類型相冊

這是最基本的一個用途,但是每次隔了幾天就忘了具體的類型。
通過PHAssetCollection的以下方法來獲取指定的相冊:

func fetchAssetCollectionsWithType(_ type: PHAssetCollectionType, subtype subtype: PHAssetCollectionSubtype, options options: PHFetchOptions?) -> PHFetchResult

這個方法需要至少指定兩個參數:

enum PHAssetCollectionType : Int {
    case Album //從 iTunes 同步來的相冊,以及用戶在 Photos 中自己建立的相冊
    case SmartAlbum //經由相機得來的相冊
    case Moment //Photos 為我們自動生成的時間分組的相冊
}

enum PHAssetCollectionSubtype : Int {
    case AlbumRegular //用戶在 Photos 中創建的相冊,也就是我所謂的邏輯相冊
    case AlbumSyncedEvent //使用 iTunes 從 Photos 照片庫或者 iPhoto 照片庫同步過來的事件。然而,在iTunes 12 以及iOS 9.0 beta4上,選用該類型沒法獲取同步的事件相冊,而必須使用AlbumSyncedAlbum。
    case AlbumSyncedFaces //使用 iTunes 從 Photos 照片庫或者 iPhoto 照片庫同步的人物相冊。
    case AlbumSyncedAlbum //做了 AlbumSyncedEvent 應該做的事
    case AlbumImported //從相機或是外部存儲導入的相冊,完全沒有這方面的使用經驗,沒法驗證。
    case AlbumMyPhotoStream //用戶的 iCloud 照片流
    case AlbumCloudShared //用戶使用 iCloud 共享的相冊
    case SmartAlbumGeneric //文檔解釋為非特殊類型的相冊,主要包括從 iPhoto 同步過來的相冊。由于本人的 iPhoto 已被 Photos 替代,無法驗證。不過,在我的 iPad mini 上是無法獲取的,而下面類型的相冊,盡管沒有包含照片或視頻,但能夠獲取到。
    case SmartAlbumPanoramas //相機拍攝的全景照片
    case SmartAlbumVideos //相機拍攝的視頻
    case SmartAlbumFavorites //收藏文件夾
    case SmartAlbumTimelapses //延時視頻文件夾,同時也會出現在視頻文件夾中
    case SmartAlbumAllHidden //包含隱藏照片或視頻的文件夾
    case SmartAlbumRecentlyAdded //相機近期拍攝的照片或視頻
    case SmartAlbumBursts //連拍模式拍攝的照片,在 iPad mini 上按住快門不放就可以了,但是照片依然沒有存放在這個文件夾下,而是在相機相冊里。
    case SmartAlbumSlomoVideos //Slomo 是 slow motion 的縮寫,高速攝影慢動作解析,在該模式下,iOS 設備以120幀拍攝。不過我的 iPad mini 不支持,沒法驗證。
    case SmartAlbumUserLibrary //這個命名最神奇了,就是相機相冊,所有相機拍攝的照片或視頻都會出現在該相冊中,而且使用其他應用保存的照片也會出現在這里。
    case Any //包含所有類型
}

有些參數的命名十分令人困惑,我每次看了都暈菜。新的 Photos Kit 框架是在 iOS 8 中推出的,主類型分為三種類型:Album,SmartAlbum 以及 Moment。然而,對于前兩者的分類我是比較困惑的。Mac 上支持智能文件夾,就是可以不管文件的物理位置而將一系列文件集合起來建立一個文件夾,可以說是物理相冊和邏輯相冊。而在 iOS 上,SmartAlbum 卻給了相機衍生的相冊,用戶收集不同照片建立的邏輯相冊被歸類到 Album 類型的 AlbumRegular 下,十分反我的直覺。在文檔中,SmartAlbum 是指內容會動態變化的相冊,這樣一來又有一個比較困惑的設計,PHAssetCollection 類有個屬性 estimatedAssetCount,可以用來快速獲取該相冊中的照片和視頻的數量,但是在 SmartAlbum 上該屬性永遠為0,動態相冊沒能實現對數量的監測。

注意,獲取指定類型的相冊時,主類型和子類型要匹配,不要串臺。如果不匹配,系統會按照 Any 子類型來處理。對于 Moment 類型,子類型使用 Any。
1.獲取用戶自己建立的相冊和文件夾(我稱之為邏輯相冊,非系統相冊和從 iTunes 同步來的相冊)有兩種方法:

PHCollection.fetchTopLevelUserCollectionsWithOptions(nil) 
PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .AlbumRegular, options: nil)

在沒有提供PHOptions的情況下,返回的PHFetchResult結果是按相冊的建立時間排序的,最新的在前面。
2.獲取相機相冊:

PHAssetCollection.fetchAssetCollectionsWithType(.SmartAlbum, subtype: .SmartAlbumUserLibrary, options: nil)

另外PHAsset的獲取方式在 iOS 8.1 后發生了一些變化。以下的兩個方法在 iOS 8.1后不再包含從 iTunes 同步以及在 iCloud 中的照片和視頻。要獲取 iOS 設備上本地的所有照片和資源只能從 PHAssetCollection 入手了。
+ fetchAssetsWithMediaType:options:
+ fetchAssetsWithOptions:

添加、刪除、編輯

對照片庫進行操作,可參見官方文檔 Requesting Changes to the Photo Library,照片庫中的資源都有對應的變更請求類:PHAssetChangeRequest, PHAssetCollectionChangeRequestPHCollectionListChangeRequest, 而這些操作的請求都要求在PHPhotoLibraryperformChanges(_ changeBlock: dispatch_block_t!, completionHandler completionHandler: ((Bool, NSError!) -> Void)!)中的 changeBlock 中執行。注意,這里只是發出請求并沒有做出實質的更改,因此想要根據更改結果更新 UI 的話不要在 completionHandler 中進行,而應該在 photoLibraryDidChange(changeInfo: PHChange!)中進行。三種變更請求中,刪除和編輯操作都比較簡單,而添加操作有需要注意的地方。

添加操作: placeholder 的用處

在相冊中添加照片:

let createAssetRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
let assetPlaceholder = createAssetRequest.placeholderForCreatedAsset
let albumChangeRequest = PHAssetCollectionChangeRequest(forAssetCollection: album)
albumChangeRequest.addAssets([assetPlaceholder])

在文件夾中添加相冊:

let fetchResult = PHCollection.fetchCollectionsInCollectionList(collectionList, options: nil)
let createSubAlbumRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollectionWithTitle(title!)
let albumPlaceholder = createSubAlbumRequest.placeholderForCreatedAssetCollection
let folderChangeRequest = PHCollectionListChangeRequest.init(forCollectionList: collectionList, childCollections: fetchResult)
folderChangeRequest?.addChildCollections([albumPlaceholder])

在文件夾中添加子文件夾:

let fetchResult = PHCollection.fetchCollectionsInCollectionList(collectionList, options: nil)
let createSubFolderRequest = PHCollectionListChangeRequest.creationRequestForCollectionListWithTitle(title!)
let subfolderPlaceholder = createSubFolderRequest.placeholderForCreatedCollectionList
let folderChangeRequest = PHCollectionListChangeRequest.init(forCollectionList: collectionList, childCollections: fetchResult)
folderChangeRequest?.addChildCollections([subfolderPlaceholder])   

處理變更

對相冊發出變更請求后,系統會通知用戶是否允許,用戶允許后才會發生實質上的變化,系統會發布通知。
首先,注冊成為PHPhotoLibrary的觀察者來接收變化通知:

PHPhotoLibrary.shareLibrary().registerChangeObserver(self)

然后,實現PHPhotoLibraryChangeObserver協議的photoLibraryDidChange(changeInfo: PHChange!)。官方有個很好的例子:Handling Changes: An Example,有以下幾點需要注意:
1.在photoLibraryDidChange(changeInfo: PHChange!)的實現里將所有處理放在主線程里處理;
2.所有PHPhotoLibrary的觀察者都會收到通知,不管觀察者本身引用的內容是否發生變化,因此要根據觀察者的情況來對通知進行過濾。從參數PHChange對象里能獲得所有的變化,通過changeDetailsForObject:changeDetailsForFetchResult:來獲取細節。changeDetailsForObject:獲取的細節只是PHObject子類對象本身的信息變化,包括是否有成員被刪除以及是否有圖像或視頻發生變化兩種信息,有用信息實在有限,要處理成員變化需要依靠后者;對一個PHFetchResult對象使用changeDetailsForFetchResult:獲取的細節中只包含該PHFetchResult對象變化的信息,可以利用這點來對通知進行過濾處理。
3.通過changeDetailsForFetchResult:獲取的PHFetchResultChangeDetails對象,包含了 FetchResult 的結果的所有變化情況以及 FetchResult 的成員變化前后的數據,需要注意的是成員變化的通知。
例如,通過

var rootCollectionsFetchResult = PHCollection.fetchTopLevelUserCollectionsWithOptions(nil)

獲取所有用戶建立的相冊和文件夾,在photoLibraryDidChange(changeInfo: PHChange!)中通過以下方法獲得PHFetchResultChangeDetails對象。

let fetchChangeDetails = changeInstance.changeDetailsForFetchResult(rootCollectionsFetchResult)

fetchChangeDetails.changedObject返回一組其內容或元數據發生變化的成員,返回的成員是更新后的成員對象。當用戶對某個文件夾內的相冊或子文件夾進行添加、刪除和編輯操作即文件夾的內容而不是文件夾本身的屬性發生變化時,通知中會該變化的信息嗎?實際上只有在文件夾中添加相冊或子文件夾時才會在fetchChangeDetails.changedObject中有所反應,而刪除成員或是修改元數據等操作都不會在通知有所反應,你需要使用其他手段來跟蹤變化。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容