每天,用 iPhone 拍攝的照片數(shù)量超過(guò)了任何相機(jī)。每年 iOS 設(shè)備上的顯示效果變得越來(lái)越好,回到 iPad 剛出現(xiàn)還沒(méi)有 Retina 顯示屏的時(shí)代,大屏幕的殺手級(jí)功能之一就是可以展示用戶照片和瀏覽器照片庫(kù)。自從相機(jī)成為 iPhone 最重要和最受歡迎的功能開(kāi)始,對(duì)能管理和加工用戶照片庫(kù)中寶貴的照片的應(yīng)用程序和工具就有著巨大的需求。
直到 2014 年夏天前,開(kāi)發(fā)者只能用 AssetsLibrary 框架訪問(wèn)日益增長(zhǎng)的用戶的照片庫(kù)。幾年以來(lái),相機(jī)應(yīng)用和照片應(yīng)用發(fā)生了顯著的變化,增加了許多新特性,包括按時(shí)刻來(lái)組織照片的方式。但與此同時(shí),AssetsLibrary 框架卻沒(méi)有跟上步伐。
隨著 iOS 8 的到來(lái),蘋(píng)果給我們提供了一個(gè)現(xiàn)代化的框架 —— PhotoKit,它比 AssetsLibrary 表現(xiàn)更好,并且擁有讓?xiě)?yīng)用和設(shè)備照片庫(kù)無(wú)縫工作的特性。
我們將從框架對(duì)象模型的鳥(niǎo)瞰圖開(kāi)始:實(shí)體和實(shí)體間的關(guān)系,獲取實(shí)體的實(shí)例,以及使用獲取的結(jié)果進(jìn)行工作。
除此之外,我們的講解還會(huì)涉及到一些在使用 AssetsLibrary 時(shí),尚未對(duì)開(kāi)發(fā)者開(kāi)放的資源元數(shù)據(jù)。
然后我們會(huì)討論加載資源的圖像數(shù)據(jù):過(guò)程本身,大量可用的選項(xiàng),一些陷阱和邊界案例。
最后,我們會(huì)談?wù)勍ㄟ^(guò)外部參與者來(lái)觀察照片庫(kù)的變化,學(xué)習(xí)如何創(chuàng)建和提交我們自己修改的改變。
PhotoKit 定義了與系統(tǒng)的 Photos 應(yīng)用內(nèi)展現(xiàn)給用戶的模型對(duì)象相一致的實(shí)體圖表。這些照片實(shí)體都是輕量級(jí)的不可變對(duì)象。所有的 PhotoKit 對(duì)象都是繼承自 PHObject 抽象基類,其公共接口只提供了一個(gè) localIdentifier 屬性。
PHAsset 表示用戶照片庫(kù)中一個(gè)單獨(dú)的資源,用以提供資源的元數(shù)據(jù)。
成組的資源叫做資源集合,用 PHAssetCollection 類表示。一個(gè)單獨(dú)的資源集合可以是照片庫(kù)中的一個(gè)相冊(cè)或者一個(gè)時(shí)刻,或者是一個(gè)特殊的“智能相冊(cè)”。這種智能相冊(cè)包括所有的視頻集合,最近添加的項(xiàng)目,用戶收藏,所有連拍照片等等。PHAssetCollection 是 PHCollection 的子類。
PHCollectionList 表示一組的 PHCollections。因?yàn)樗旧砭褪?PHCollection,所以集合列表可以包含其他集合列表,它們?cè)试S復(fù)雜的集合繼承。實(shí)際上,我們可以在照片應(yīng)用的時(shí)刻欄目中看到它:照片 --- 時(shí)刻 --- 精選 --- 年度,就是一個(gè)例子。
那些熟悉 AssetsLibrary 框架的開(kāi)發(fā)者可能會(huì)記得 AssetsLibrary 可以用一些特定屬性來(lái)找到需要的資源,其中一個(gè)必須枚舉用戶資源庫(kù)來(lái)獲得匹配的資源。不得不承認(rèn),這個(gè) API 雖然提供了一些縮小搜索域的方法,但還是十分低效。
而與之形成鮮明對(duì)比,PhotoKit 實(shí)體的實(shí)例是通過(guò)獲取得到的。那些熟悉 Core Data 的人,會(huì)覺(jué)得和 PhotoKit 在概念和描述都比較接近。
獲取操作是由上面描述的實(shí)體的類方法實(shí)現(xiàn)的。要使用哪個(gè)類/方法,取決于問(wèn)題所在范圍和你展示與遍歷照片庫(kù)的方式。所有獲取方法的命名都是相似的:class func fetchXXX(..., options: PHFetchOptions) -> PHFetchResult 。options 參數(shù)給了我們一個(gè)對(duì)結(jié)果進(jìn)行過(guò)濾和排序的途徑,這和 NSFetchRequest 的 predicate 與 sortDescriptors 參數(shù)類似。
你可能已經(jīng)注意到了這些獲取操作不是異步的。它們返回了一個(gè) PHFetchResult 對(duì)象,可以用類似 NSArray 的接口來(lái)訪問(wèn)結(jié)果內(nèi)的集合。它會(huì)按需動(dòng)態(tài)加載內(nèi)容并且緩存最近請(qǐng)求的內(nèi)容。這個(gè)行為和設(shè)置了 batchSize 屬性的 NSFetchRequest 返回的結(jié)果數(shù)組相似。對(duì)于 PHFetchResult 來(lái)說(shuō),沒(méi)有辦法用參數(shù)來(lái)指定這個(gè)行為,但是官網(wǎng)文檔保證 “即使在處理大量的返回結(jié)果時(shí),依然能夠有最好的表現(xiàn)”。
就算滿足請(qǐng)求的照片庫(kù)內(nèi)容發(fā)生了改變,獲取方法所返回的 PHFetchResult 對(duì)象是不會(huì)自動(dòng)更新。在后面的小節(jié)中,我們會(huì)介紹如何對(duì)返回的 PHFetchResult 對(duì)象的改變進(jìn)行觀察并處理更新內(nèi)容。
你可能會(huì)發(fā)現(xiàn)你已經(jīng)設(shè)計(jì)了一個(gè)可以操作資源集合的組件,并且你還希望它能夠處理任意一組的資源。PhotoKit 通過(guò)臨時(shí)資源集合,讓我們可以輕松做到這點(diǎn)。
你可以通過(guò) PHAsset 對(duì)象數(shù)組或是包含資源的 PHFetchResult 對(duì)象來(lái)創(chuàng)建臨時(shí)資源集合。創(chuàng)建的操作在 PHAssetCollection 的 transientAssetCollectionWithAssets(...) 和 transientAssetCollectionWithFetchResult(...) 工廠方法內(nèi)完成。這些方法創(chuàng)建出來(lái)的對(duì)象可以像其它的 PHAssetCollection 對(duì)象一樣使用。盡管如此,這些集合不會(huì)被存儲(chǔ)到用戶照片庫(kù),自然也不會(huì)在照片應(yīng)用中展示出來(lái)。
和資源集合相似,你可以用 PHCollectionList 中的 transientCollectionListWithXXX(...) 工廠方法來(lái)創(chuàng)建臨時(shí)集合列表。
當(dāng)你要合并兩個(gè)獲取請(qǐng)求時(shí),你就會(huì)發(fā)現(xiàn)這個(gè)東西非常有用。
正如文章開(kāi)頭提到的,PhotoKit 提供了額外的關(guān)于用戶資源的元數(shù)據(jù),而這些數(shù)據(jù)在以前使用 ALAssetsLibrary 框架中是沒(méi)有辦法訪問(wèn),或者很難訪問(wèn)到。
你可以使用照片資源的 mediaSubtypes 屬性驗(yàn)證資源庫(kù)中的圖像在捕捉時(shí)是否開(kāi)啟了 HDR,拍攝時(shí)是否使用了相機(jī)應(yīng)用的全景模式。
要驗(yàn)證一個(gè)資源是否被用戶標(biāo)記為收藏或被隱藏,只要檢查 PHAsset 實(shí)例的 favorite 和 hidden 屬性即可。
對(duì)于一個(gè)資源,如果其 PHAsset 的 representsBurst 屬性為 true,則表示這個(gè)資源是一系列連拍照片中的代表照片 (多張照片是在用戶按住快門(mén)時(shí)拍攝的)。它還有一個(gè)屬性是 burstIdentifier,如果想要獲取連拍照片中的剩余的其他照片,可以通過(guò)將這個(gè)值傳入 fetchAssetsWithBurstIdentifier(...) 方法來(lái)獲取。
用戶可以在連拍的照片中做標(biāo)記;此外,系統(tǒng)也會(huì)自動(dòng)用各種試探來(lái)標(biāo)記用戶可能會(huì)選擇的潛在代表照片。這個(gè)元數(shù)據(jù)是可以通過(guò) PHAsset 的 burstSelectionTypes 屬性來(lái)訪問(wèn)。這個(gè)屬性是用三個(gè)常量組成的位掩碼:.UserPick 表示用戶手動(dòng)標(biāo)記的資源,.AutoPick 表示用戶可能標(biāo)記的潛在資源,.None 表示沒(méi)有標(biāo)記的資源。
http://wiki.jikexueyuan.com/project/objc/images/21-12.jpg" alt="" />
這個(gè)屏幕快照顯示了,照片應(yīng)用是如何在連拍的照片中自動(dòng)標(biāo)記用戶可能標(biāo)記的潛在資源。
在處理用戶照片庫(kù)的過(guò)去幾年中,開(kāi)發(fā)者創(chuàng)造了上百 (如果沒(méi)有上千) 的小技巧來(lái)提高照片加載和展示的效率。這些技巧處理請(qǐng)求的派發(fā)和取消,圖像大小的修改和裁剪,緩存等等。PhotoKit 提供了一個(gè)可以用更加便捷和現(xiàn)代的 API 做了所有這些操作的類:PHImageManager 。
圖像請(qǐng)求是通過(guò) requestImageForAsset(...) 方法派發(fā)的。這個(gè)方法接受一個(gè) PHAsset,可以設(shè)置返回圖像的大小和圖像的其它可選項(xiàng) (通過(guò) PHImageRequestOptions 參數(shù)對(duì)象設(shè)置),以及結(jié)果回調(diào) (result handler)。這個(gè)方法的返回值可以用來(lái)在所請(qǐng)求的數(shù)據(jù)不再被需要時(shí)取消這個(gè)請(qǐng)求。
奇怪的是,對(duì)返回圖像的尺寸定義和裁剪的參數(shù)是分布在兩個(gè)地方的。targetSize 和 contentMode 這倆參數(shù)會(huì)被直接傳入 requestImageForAsset(...) 方法內(nèi)。這個(gè) content Mode 和 UIView 的 contentMode 參數(shù)類似,決定了照片應(yīng)該以按比例縮放還是按比例填充的方式放到目標(biāo)大小內(nèi)。注意:如果不對(duì)照片大小進(jìn)行修改或裁剪,那么方法參數(shù)是 PHImageManagerMaximumSize 和 PHImageContentMode.Default 。
此外,PHImageRequestOptions 還提供了一些方式來(lái)確定圖像管理器該以怎樣的方式來(lái)重新設(shè)置圖像大小。resizeMode 屬性可以設(shè)置為 .Exact (返回圖像必須和目標(biāo)大小相匹配),.Fast (比 .Exact 效率更高,但返回圖像可能和目標(biāo)大小不一樣) 或者 .None。還有個(gè)值得一提的是,normalizedCroppingMode 屬性讓我們確定圖像管理器應(yīng)該如何裁剪圖像。注意:如果設(shè)置了 normalizedcroppingMode 的值,那么 resizeMode 需要設(shè)置為 .Exact。
默認(rèn)情況下,如果圖像管理器決定要用最優(yōu)策略,那么它會(huì)在將圖像的高質(zhì)量版本遞送給你之前,先傳遞一個(gè)較低質(zhì)量的版本。你可以通過(guò) deliveryMode 屬性來(lái)控制這個(gè)行為;上面所描述的默認(rèn)行為的值為 .Opportunistic。如果你只想要高質(zhì)量的圖像,并且可以接受更長(zhǎng)的加載時(shí)間,那么將屬性設(shè)置為 .HighQualityFormat。如果你想要更快的加載速度,且可以犧牲一點(diǎn)圖像質(zhì)量,那么將屬性設(shè)置為 .FastFormat。
你可以使用 PHImageRequestOptions 的 synchronous 屬性,讓 requestImage... 系列的方法變成同步操作。注意:當(dāng) synchronous 設(shè)為 true 時(shí),deliveryMode 屬性就會(huì)被忽略,并被當(dāng)作 .HighQualityFormat 來(lái)處理。
在設(shè)置這些參數(shù)時(shí),一定要考慮到你的一些用戶有可能開(kāi)啟了 iCloud 照片庫(kù),這點(diǎn)非常重要。PhotoKit 的 API 不一定會(huì)對(duì)設(shè)備的照片和 iCloud 上照片進(jìn)行區(qū)分 —— 它們都用同一個(gè) requestImage 方法來(lái)加載。這意味著任意一個(gè)圖像請(qǐng)求都有可能是一個(gè)通過(guò)蜂窩網(wǎng)絡(luò)來(lái)進(jìn)行的非常緩慢的網(wǎng)絡(luò)請(qǐng)求。當(dāng)你要用 .HighQualityFormat 或者做一個(gè)同步請(qǐng)求的時(shí)候,要牢記這個(gè)。注意:如果你想要確保請(qǐng)求不經(jīng)過(guò)網(wǎng)絡(luò),可以將 networkAccessAllowed 設(shè)為 false 。
另一個(gè)和 iCloud 相關(guān)的屬性是 progressHandler。你可以將它設(shè)為一個(gè) PHAssetImageProgressHandler 的 block,當(dāng)從 iCloud 下載照片時(shí),它就會(huì)被圖像管理器自動(dòng)調(diào)用。
PhotoKit 允許應(yīng)用對(duì)照片進(jìn)行無(wú)損的修改。對(duì)編輯過(guò)的照片,系統(tǒng)會(huì)對(duì)單獨(dú)保存一份原始照片的拷貝和針對(duì)應(yīng)用的調(diào)整數(shù)據(jù)。當(dāng)用圖像管理器獲取資源時(shí),你可以指定哪個(gè)版本的圖像資源應(yīng)該通過(guò) result handler 被遞送。這可以通過(guò)設(shè)置 version 屬性來(lái)做到:.Current 會(huì)遞送包含所有調(diào)整和修改的圖像;.Unadjusted 會(huì)遞送未被施加任何修改的圖像;.Original 會(huì)遞送原始的、最高質(zhì)量的格式的圖像 (例如 RAW 格式的數(shù)據(jù)。而當(dāng)將屬性設(shè)置為 .Unadjusted 時(shí),會(huì)遞送一個(gè) JPEG)。
你可以在 Sam Davies 的文章《照片擴(kuò)展》中,閱讀框架中更多關(guān)于這方面的內(nèi)容。
結(jié)果回調(diào)是一個(gè)包含了一個(gè) UIImage 變量和一個(gè) info 字典作為參數(shù)的 block。根據(jù)參數(shù)和請(qǐng)求的選項(xiàng),在請(qǐng)求的整個(gè)生命周期,它可以被圖像管理器多次調(diào)用。
info 字典提供了關(guān)于當(dāng)前請(qǐng)求狀態(tài)的信息,比如:
networkAccessAllowed 設(shè)置成 false,那么就必須重新請(qǐng)求圖像) —— PHImageResultIsInCloudKey 。UIImage 是否是最終結(jié)果的低質(zhì)量格式。當(dāng)高質(zhì)量圖像正在下載時(shí),這個(gè)可以讓你給用戶先展示一個(gè)預(yù)覽圖像 —— PHImageResultIsDegradedKey。PHImageResultRequestIDKey 和 PHImageCancelledKey。PHImageErrorKey。這些值可以讓你更新你的 UI 來(lái)告知用戶,和上面討論到的 progressHandler 一起,來(lái)表示出它們的加載狀態(tài)。
當(dāng)圖像即將要展示在屏幕上時(shí),比如當(dāng)要在一組滾動(dòng)的 collection 視圖上展示大量的資源圖像的縮略圖時(shí),預(yù)先將一些圖像加載到內(nèi)存中有時(shí)是非常有用的。PhotoKit 提供了一個(gè) PHImageManager 的子類來(lái)處理這種特定的使用場(chǎng)景 —— PHImageCachingManager。
PHImageCachingManager 提供了一個(gè)關(guān)鍵方法 —— startCachingImagesForAssets(...)。你傳入一個(gè) PHAssets 類型的數(shù)組,一些請(qǐng)求參數(shù),以及一些請(qǐng)求單個(gè)圖像時(shí)即將用到的可選項(xiàng)。此外,還有一些方法可以讓你通知緩存管理器來(lái)停止緩存特定資源列表,以及停止緩存所有圖像。
allowsCachingHighQualityImages 屬性可以讓你指定圖像管理器是否應(yīng)該準(zhǔn)備高質(zhì)量圖像。當(dāng)緩存一個(gè)較短和不變的資源列表時(shí),默認(rèn) true 的屬性表現(xiàn)很好。但當(dāng)要在 collection 視圖上快速滑動(dòng)時(shí)做緩存操作的話,最好將它設(shè)置成 false 。
注意:以我的經(jīng)驗(yàn),當(dāng)用戶正在有大量資源的 collection 視圖上極其快速的滑動(dòng)時(shí),使用緩存管理器會(huì)損害滑動(dòng)的表現(xiàn)效果。為這種特定的使用場(chǎng)景定制一個(gè)緩存行為是極其重要的。緩存窗口的大小,移動(dòng)緩存窗口的時(shí)機(jī)和頻率,allowsCachingHighQualityImages 的屬性值 —— 這些參數(shù)都要在目標(biāo)硬件上的真實(shí)照片庫(kù)中仔細(xì)地調(diào)節(jié),并測(cè)試表現(xiàn)效果。更進(jìn)一步,你可以考慮在用戶行為的基礎(chǔ)上,動(dòng)態(tài)的設(shè)置這些參數(shù)。
最后,除了請(qǐng)求普通的 UIImage 之外,PHImageManager 提供了另一個(gè)方法可以返回 NSData 對(duì)象類型的資源數(shù)據(jù),包括它的通用類型標(biāo)識(shí)符,圖像的展示方向。這個(gè)方法返回了這個(gè)資源的最多的信息。
我們已經(jīng)討論了在用戶照片庫(kù)中請(qǐng)求資源的元數(shù)據(jù),但是到目前為止,我們沒(méi)提及如何更新我們獲取的數(shù)據(jù)。照片庫(kù)本質(zhì)上是一大堆可變的狀態(tài),而第一節(jié)中提到的照片實(shí)體是不可變的對(duì)象。PhotoKit 可以讓你接收你需要的、關(guān)于照片庫(kù)變動(dòng)的所有信息,以正確更新你的緩存狀態(tài)。
首先,你需要通過(guò)共享的 PHPhotoLibrary 對(duì)象,用 registerChangeObserver(...) 方法注冊(cè)一個(gè)變化觀察者 (這個(gè)觀察者要遵從 PHPhotoLibraryChangeObserver 協(xié)議)。只要另一個(gè)應(yīng)用或者用戶在照片庫(kù)中做的修改影響了你在變化前獲取的任何資源或資源集合的話,變化觀察者的 photoLibraryDidChange(...) 方法都會(huì)被調(diào)用。這個(gè)方法只有一個(gè) PHChange 類型的參數(shù),你可以用它來(lái)驗(yàn)證這些變化是否和你所感興趣的獲取對(duì)象有關(guān)聯(lián)。
PHChange 提供了幾個(gè)方法,讓你可以通過(guò)傳入任何你感興趣的 PHObject 對(duì)象或 PHFetchResult 對(duì)象來(lái)追蹤它們的變化。這幾個(gè)方法是 changeDetailsForObject(...) 和 changeDetailsForFetchResult(...) 。如果沒(méi)有任何變化,這些方法會(huì)返回 nil,否則你可以借助 PHObjectChangeDetails 或 PHFetchResultChangeDetails 對(duì)象來(lái)觀察這些變化。
PHObjectChangeDetails 提供了一個(gè)對(duì)最新的照片實(shí)體對(duì)象的引用,以及告訴你對(duì)象的圖像數(shù)據(jù)是否曾變化過(guò)、對(duì)象是否曾被刪除過(guò)的布爾值。
PHFetchResultChangeDetails 封裝了施加在你之前通過(guò)獲取所得到的 PHFetchResult 上的變化的信息。PHFetchResultChangeDetails 是為了盡可能簡(jiǎn)化 CollectionView 或 TableView 的更新操作而設(shè)計(jì)的。它的屬性恰好映射到你在使用一個(gè)典型的 CollectionView 的 update handler 時(shí)所需要的信息。注意,若要正確的更新 UITableView/UICollectionView,你必須以正確順序來(lái)處理變化,那就是:RICE —— removedIndexes,insertedIndexes,changedIndexes,enumerateMovesWithBlock (如果 hasMoves 為 true 的話)。另外,PHFetchResultChangeDetails 的 hasIncrementalChanges 屬性可以被設(shè)置成 false,這意味著舊的獲取結(jié)果應(yīng)該全部被新的值代替。這種情況下,你應(yīng)該調(diào)用 UITableView/UICollectionView 的 reloadData。
注意:沒(méi)有必要以集中的方式處理變化。如果你應(yīng)用中的多個(gè)組件需要處理照片實(shí)體,那么它們每個(gè)都要有自己的 PHPhotoLibraryChangeObserver 。接著組件就能靠自己查詢 PHChange 對(duì)象,檢測(cè)是否需要 (以及如何) 更新它們自己的狀態(tài)。
現(xiàn)在我們已經(jīng)知道了如何觀察用戶和其他應(yīng)用造成的變化,我們來(lái)嘗試一下自己進(jìn)行改變。
用 PhotoKit 在照片庫(kù)做改變,說(shuō)到底其實(shí)是先創(chuàng)建了一個(gè)鏈接到某個(gè)資源或者資源集合的變化請(qǐng)求對(duì)象,再設(shè)置請(qǐng)求對(duì)象的相關(guān)屬性或調(diào)用合適的方法來(lái)描述你想要提交的變化。這個(gè)必須通過(guò) performChanges(...) 方法,在提交到共享的 PHPhotoLibrary 的 block 內(nèi)完成。注意:你需要準(zhǔn)備好在 performChanges 方法的 completion block 里處理失敗的情況。雖然處理的是能被多個(gè)參與者 (如你的應(yīng)用,用戶,其他應(yīng)用,照片擴(kuò)展等) 改變的狀態(tài),但這個(gè)方式能提供安全性,也相對(duì)易用。
想要修改資源,需要?jiǎng)?chuàng)建一個(gè) PHAssetChangeRequest 。然后你就可以修改創(chuàng)建創(chuàng)建日期,資源位置,以及是否將隱藏資源,是否將資源看做用戶收藏等。此外,你還可以從用戶的庫(kù)里刪除資源。
類似地,若要修改資源集合或集合列表,需要?jiǎng)?chuàng)建一個(gè) PHAssetCollectionChangeRequest 或 PHCollectionListChangeRequest 對(duì)象。然后你就可以修改集合標(biāo)題,添加或刪除集合成員,或者完全刪除集合。
在你的變化提交到用戶照片庫(kù)前,系統(tǒng)會(huì)向用戶展示一個(gè)明確的獲取權(quán)限的警告框。
創(chuàng)建一個(gè)新資源的做法和修改已存在的資源類似。只要用 creationRequestForAssetFromXXX(...) 工廠方法,來(lái)創(chuàng)建變化請(qǐng)求,并傳入資源圖像數(shù)據(jù) (或一個(gè) URL)。如果你需要對(duì)新建的資源做額外的修改,你可以用創(chuàng)建變化請(qǐng)求的 placeholderForCreatedAsset 屬性。它會(huì)返回一個(gè)可用的 placeholder 來(lái)代替“真實(shí)的” PHAsset 引用。
我已經(jīng)討論了 PhotoKit 的基礎(chǔ)知識(shí),但仍然還有非常多的東西等著我們?nèi)グl(fā)掘。你可以通過(guò)查看示例的各處的代碼,觀看 WWDC session 視頻學(xué)習(xí)更多內(nèi)容,發(fā)掘更深的知識(shí),然后寫(xiě)一些自己的代碼!PhotoKit 為 iOS 開(kāi)發(fā)者開(kāi)啟了通往新世界可能性,在未來(lái)的數(shù)月或者數(shù)年里,我們肯定會(huì)看到更多基于這個(gè)基礎(chǔ)構(gòu)建的富有創(chuàng)造性的優(yōu)秀產(chǎn)品。