在 iOS 8 發(fā)布時,蘋果把六種全新擴展功能介紹給全世界,它們史無前例的提供了訪問操作系統(tǒng)的可行性?,F(xiàn)在,開發(fā)者可以利用照片擴展來為系統(tǒng)相機應用增加功能。
用戶使用照片編輯擴展的流程并不簡單。從選擇編輯的照片開始,需要點擊三次才能啟動,其中一步驟是非常小一個按鈕:
http://wiki.jikexueyuan.com/project/objc/images/21-13.png" alt="" />
然而,這類擴展給開發(fā)者提供了為用戶創(chuàng)造無縫體驗,創(chuàng)建一致的方法來管理照片的絕佳的機會。
本文在了解更詳細的編輯工作流程之前,將先簡單討論如何創(chuàng)建擴展以擴展的生命周期,我們會通過常見的相關問題和場景來創(chuàng)建照片編輯擴展從而得出結(jié)論。
本文的示例項目 Filtster 演示了如何創(chuàng)建自己的圖片編輯擴展。它詮釋使用了數(shù)個 Core Image 濾鏡完成簡單的圖像過濾效果。完整 Filtster 項目代碼 可以在 GitHub 上找到。
所有擴展必須包含在一個功能齊全的 iOS 應用程序之內(nèi),照片編輯擴展也不例外。這可能意味著你必須做很多令人吃驚的自定義 Core Image 濾鏡的才能讓它到達用戶手中。蘋果如何嚴格審查還有待觀察,因為蘋果商店內(nèi)的大多數(shù)有圖片編輯功能的應用都是在 iOS 8 引入之前就已經(jīng)存在了的。
為了創(chuàng)建新的圖片編輯擴展,需要為已有的 iOS 項目添加新的 target,擴展 target 模板如下:
http://wiki.jikexueyuan.com/project/objc/images/21-14.png" alt="" />
模板由三部分組成:
http://wiki.jikexueyuan.com/project/objc/images/21-15.png" alt="" />
雖然 storyboard 默認不包含 size classes,系統(tǒng)將允許你選擇并激活該功能,雖然沒有明顯的原因來阻止你使用手動布局,但蘋果還是強烈建議使用 Auto Layout 來創(chuàng)建照片編輯擴展。如果你忽略蘋果的建議你將不得不面對很多潛在風險。
NSExtension 鍵值是一個字典,它包含擴展所需要的配置:http://wiki.jikexueyuan.com/project/objc/images/21-16.png" alt="" />
`NSExtensionPointIdentifier` 實體告訴系統(tǒng)這是一個使用 `com.apple.photo-editing` 作為值的照片編輯擴展。唯一特殊的 key 是 `PHSupportedMediaTypes`,它指明可以被操作的媒體類型。在默認情況下,這是一個包含 `Image` 實體的數(shù)組,當然你也可添加 `Video` 選項。
PHContentEditingController 協(xié)議,其中包含了圖片編輯擴展需要的生命周期方法。更多詳情見本文下個部分。值得注意的是不要忘記提供菜單內(nèi)的擴展的圖標:
http://wiki.jikexueyuan.com/project/objc/images/21-17.png" alt="" />
圖標通過宿主 app 的資源目錄內(nèi)的 App Icon 提供。這里文檔有些讓人迷惑,它暗示你必須在擴展本身里來提供圖標。然而,盡管我們可以提供一個這樣的圖標,但擴展將不會使用選擇它。這一點有些爭議,因為蘋果指定與擴展相關的圖標必須與容器應用程序的相同。
照片編輯擴展建立于 Photos 框架之上的,這意味著編輯不是破壞性的。當一個照片資源被編輯的時候,原始文件始終沒有被修改,編輯的結(jié)果將作為副本被保存下來。另外,語義細節(jié)包含了如何重新編輯并保存調(diào)整后的數(shù)據(jù)。這個數(shù)據(jù)的意思是編輯可以基于原始文件重新來過。當你實現(xiàn)圖片編輯擴展的時候,你只負責構(gòu)建你自己的數(shù)據(jù)對象。
PHAdjustmentData 類含有編輯所需參數(shù),以及兩個格式化的屬性 (formatIdentifier 和 formatVersion) 用來確定當前編輯擴展針對于之前的編輯過的照片的兼容性。它們兩個都是字符串類型,另外 formatIdentifier 規(guī)定為反向域名解析格式。這兩個屬性讓你靈活的創(chuàng)建一套圖像編輯的應用程序以及擴展,每一種都可以用另一種表示。另外 data 屬性是 NSData 類型??梢员挥脕戆茨愕男枰鎯U展操作的細節(jié),以便讓你的擴展能繼續(xù)編輯。
當用戶使用你的擴展來編輯照片的時候,系統(tǒng)會實例化你的視圖控制器并且初始化照片編輯的生命周期。如果照片之前曾經(jīng)被編輯,它首先會調(diào)用 canHandleAdjustmentData(_:) 方法,同時為你提供一個 PHAdjustmentData 對象。因此,你的擴展是否可以處理之前編輯過的數(shù)據(jù)就很重要,這將決定框架發(fā)送的下一個生命周期的方法是什么。
一旦系統(tǒng)決定提供原始圖片還是之前就被渲染編輯過的圖片,接下來將會調(diào)用 startContentEditingWithInput(_:, placeholderImage:)。輸入是一個類型為 PHContentEditingInput 的對象,其中包含了地理位置,創(chuàng)建時間以及媒體類型等來自于原始資源的元數(shù)據(jù),以及你需要編輯的資源細節(jié)。除了原始尺寸的輸入圖片的路徑以外,輸入對象還包含一個 displaySizedImage 表示相同的圖片數(shù)據(jù),但是根據(jù)屏幕尺寸進行了適當縮放。這意味著交互編輯可以在較低分辨率下進行,以此來確保擴展可以保持迅速響應操作并節(jié)省能量。
下面是實現(xiàn)方法
func startContentEditingWithInput(contentEditingInput: PHContentEditingInput?,
placeholderImage: UIImage) {
input = contentEditingInput
filter.inputImage = CIImage(image: input?.displaySizeImage)
if let adjustmentData = contentEditingInput?.adjustmentData {
filter.importFilterParameters(adjustmentData.data)
}
vignetteIntensitySlider.value = Float(filter.vignetteIntensity)
...
}
上面的實現(xiàn)中存儲了 contentEditingInput,因為要完成編輯并從調(diào)整后的數(shù)據(jù)導入濾鏡參數(shù)的時候我們會需要它。
如果你的 canHandleAdjustmentData(_:) 返回 true,startContentEditingWithInput(_:, placeholderImage:) 將會提供原始圖片,然后你的擴展需要根據(jù)調(diào)整后的數(shù)據(jù)來重新創(chuàng)建編輯過的圖片。如果這是一個耗時操作,那么 placeholderImage 將提供一個上次編輯渲染后的臨時圖片來讓你暫時使用。
在這個階段,用戶將通過擴展界面的交互來控制編輯的進程。因為擴展包含一個視圖控制器,你可以使用任何 UIKit 來實現(xiàn)它。示例項目使用了 Core Image 的濾鏡鏈來完成編輯,所以界面使用了一個自定義的 GLKView 子類來減少 CPU 的負載。
在完成編輯時,用戶可以選擇照片界面提供的取消或者完成按鈕。如果想讓用戶確定是否取消尚未保存的編輯內(nèi)容,shouldShowCancelConfirmation 屬性需要重寫并返回 true:
http://wiki.jikexueyuan.com/project/objc/images/21-18.png" alt="" />
如果需要取消操作,cancelContentEditing 方法將被調(diào)用來允許你清空所有臨時數(shù)據(jù)。
一旦用戶決定保存編輯操作,并且點擊了完成按鈕,finishContentEditingWithCompletionHandler(_:) 將會被調(diào)用。在這個時候,原始尺寸圖像需要用與當前顯示的圖片相同設置來編輯,并保存調(diào)整后的數(shù)據(jù)。
在這時,你可以通過在編輯過程開始時提供的 PHContentEditingInput 對象內(nèi)的 fullSizeImageURL 來獲取原始尺寸的圖片。
要完成編輯,我們需要調(diào)用提供的回調(diào)函數(shù),并提供一個從輸入創(chuàng)建的 PHContentEditingOutput 對象。這個輸出對象還包含了一個 renderedContentURL 屬性,用來指定你應該把輸出的 JPEG 數(shù)據(jù)存放在哪里:
func finishContentEditingWithCompletionHandler(completionHandler: ((PHContentEditingOutput!) -> Void)!) {
// 在后臺隊列渲染并提供輸出。
dispatch_async(dispatch_get_global_queue(CLong(DISPATCH_QUEUE_PRIORITY_DEFAULT), 0)) {
// 從編輯輸入創(chuàng)建編輯輸出。
let output = PHContentEditingOutput(contentEditingInput: self.input)
// 提供調(diào)整后的數(shù)據(jù)并且渲染輸出到指定位置。
let adjustmentData = PHAdjustmentData(formatIdentifier: self.filter.filterIdentifier,
formatVersion: self.filter.filterVersion, data: self.filter.encodeFilterParameters())
output.adjustmentData = adjustmentData
// 寫入 JPEG 圖片
let fullSizeImage = CIImage(contentsOfURL: self.input?.fullSizeImageURL)
UIGraphicsBeginImageContext(fullSizeImage.extent().size);
self.filter.inputImage = fullSizeImage
UIImage(CIImage: self.filter.outputImage)?.drawInRect(fullSizeImage.extent())
let outputImage = UIGraphicsGetImageFromCurrentImageContext()
let jpegData = UIImageJPEGRepresentation(outputImage, 1.0)
UIGraphicsEndImageContext()
jpegData.writeToURL(output.renderedContentURL, atomically: true)
// 調(diào)用完成回調(diào)提交編輯后的圖片。
completionHandler?(output)
}
}
一旦對 completionHandler 返回,你就可以清空臨時數(shù)據(jù),并且修改后的文件已經(jīng)準備好從擴展返回。
與創(chuàng)建圖片編輯擴展相關的內(nèi)容其中一些可能有些復雜,本節(jié)內(nèi)容將介紹最重要的幾個。
PHAdjustmentData 是一個只包含三個屬性的簡單類,但是想要用好的話,依然需要遵循一些規(guī)則。蘋果建議使用反向域名解析格式來指定 formatIdentifier,但是 formatVersion 和 data 如何使用將由你自己決定。
重要的是要確保你不同版本圖片編輯擴展的兼容性,所以我們需要類似語義化版本這樣能提供靈活的管理產(chǎn)品的生命周期的方式。你可以以自己的方式進行解析,也可以依賴于像 SemverKit 之類的第三方框架提供的功能。
最后對于調(diào)整數(shù)據(jù)要說的是 data 本身,它是一個 NSData 數(shù)據(jù)對象。蘋果提供的唯一建議是它應該用來存放重建編輯時所需要的的設定,而不是編輯本身,這是因為 PHAdjustmentData 對象的尺寸是受 Photo 框架限制的。
對于不是很復雜的擴展 (比如 Filtster),這個數(shù)據(jù)可以是簡單地對一個字典歸檔,代碼如下:
public func encodeFilterParameters() -> NSData {
var dataDict = [String : AnyObject]()
dataDict["vignetteIntensity"] = vignetteIntensity
...
return NSKeyedArchiver.archivedDataWithRootObject(dataDict)
}
接著提供解析方式:
public func importFilterParameters(data: NSData?) {
if let data = data {
if let dataDict = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [String : AnyObject] {
vignetteIntensity = dataDict["vignetteIntensity"] as? Double ?? vignetteIntensity
...
}
}
}
這里,這兩個方法存在于共享的 FiltsterFilter 類中,同時這個類也負責確定調(diào)整數(shù)據(jù)的兼容性:
public func supportsFilterIdentifier(identifier: String, version: String) -> Bool
return identifier == filterIdentifier && version == filterVersion
}
如果你有更復雜的需求,你可以創(chuàng)建一個自定義的設置類,讓它支持 NSCoding 協(xié)議并用類似的方式進行歸檔。
用戶需要可以將互不兼容的照片編輯串聯(lián)起來 -- 如果當前擴展無法理解調(diào)整數(shù)據(jù)的話,一張預先渲染好的圖像將被作為輸入。比如你可以使用系統(tǒng)的裁剪工具先對圖片進行裁剪,然后再在你的自定義照片編輯擴展中使用。在你存儲編輯后的圖像時,與之綁定的編輯數(shù)據(jù)只會包含最近的編輯的細節(jié)。你可以將之前的不兼容的編輯的調(diào)整數(shù)據(jù)保存到你的輸出調(diào)整數(shù)據(jù)中,這樣你就可以為濾鏡鏈中你的階段實現(xiàn)還原功能。Photo 框架提供的還原功能將移除所有編輯,并把照片恢復到原始狀態(tài):
http://wiki.jikexueyuan.com/project/objc/images/21-19.png" alt="" />
照片編輯擴展作為一個嵌入式二進制文件包含在容器應用中。因為蘋果要求這個容器應用必須有完整功能,因此你創(chuàng)建的照片編輯擴展,很可能與容器應用有相同的功能。你可能會希望在應用擴展和容器之間共享代碼和數(shù)據(jù)。
共享代碼通過 iOS 8 新功能 -- 創(chuàng)建 Cocoa Touch 框架 target 來實現(xiàn)。你可以向其中添加共用的功能,例如濾鏡鏈和自定義視圖類,并在應用和擴展中同時使用。
值得注意的是因為用于創(chuàng)建擴展,你必須在 Target 設置界面將 API 兼容性限制為僅擴展可用:
http://wiki.jikexueyuan.com/project/objc/images/21-20.png" alt="" />
共享數(shù)據(jù)的需求明顯要少很多,在許多情況下并不存在。然而如果需要,你可以通過把應用和擴展都添加到一個關聯(lián)到你的開發(fā)者賬號的 app group 中的方式,來創(chuàng)建一個共享容器 (shared container)。共享容器代表的是磁盤上的一塊共享的空間,你可以使用任何你喜歡的方式使用它,比如 NSUserDefaults,SQLite 或者寫文件。
Xcode 調(diào)試雖然有一些潛在癥結(jié),但已經(jīng)相當友好了。選擇擴展的 scheme 并編譯運行,接著會詢問你希望啟動哪一個應用,因為圖片編輯擴展只能在系統(tǒng)照片應用中實現(xiàn),所以你應該選擇照片應用:
http://wiki.jikexueyuan.com/project/objc/images/21-21.png" alt="" />
如果這么做啟動的是你的容器應用的話,你可以編輯擴展 scheme 設置 executable 為 Ask on Launch 來解決。
Xcode 然后會等待你打開你的照片編輯擴展,然后將調(diào)試器掛載上去。從這時開始,你就可以用調(diào)試標準 iOS 應用的方式來調(diào)試擴展了。將調(diào)試器附加到擴展可能需要一些時間,所以當你激活擴展時,擴展可能會失去響應一段時間。如果你想評估啟動時間的話,可以在 release 模式下運行它。
性能分析和調(diào)試類似,分析器在擴展開始執(zhí)行后附加上去。你可以更新擴展相關 scheme 指定 Xcode 詢問應該啟動哪一個應用來執(zhí)行分析。
擴展不是一個全功能 iOS 應用,因此訪問系統(tǒng)資源時要受到限制。更特別的是,如果用戶使用太多內(nèi)存,系統(tǒng)將優(yōu)先關閉擴展進程。我們無法確定具體的內(nèi)存限制,因為內(nèi)存管理是由 iOS 內(nèi)部處理的,但有這肯定是基于像是設備,宿主應用,以及其他應用程序的內(nèi)存壓力這些因素的。所以其實并沒有硬性的限制,但我們還是應該盡量減少內(nèi)存占用。
圖片處理是一個高內(nèi)存操作,特別是處理的對象是來自 iPhone 相機的高清晰度圖片。你需要做幾件事情來確保照片編輯擴展的內(nèi)存使用量降到最低。
因為圖像編輯本身肯定就需要高內(nèi)存,所以與其他擴展相比,照片編輯擴展的可用內(nèi)存要多那么一些。在 ad hoc 測試中,圖片編輯擴展可以使用高于 100 MB 內(nèi)存。鑒于來自 800 萬像素相機的照片大約 22MB,所以這個內(nèi)存量對于大多數(shù)圖片編輯擴展來說是夠用的。
iOS 8 之前,第三方開發(fā)者無法在他自己應用程序之外向用戶提供功能。擴展的出現(xiàn)徹底改變這一狀況,特別是照片編輯擴展允許你把代碼運行于照片應用核心中。盡管多次點擊的流程略顯復雜,但照片編輯擴展使用 Photo 框架的功能提供了連貫和集成的用戶體驗。
可恢復的編輯一直是像 Aperture 或 Lightroom 這樣的桌面應用的殺手級功能。而現(xiàn)在在 iOS 中使用 Photo 框架,也可以為這個功能創(chuàng)建一個通用架構(gòu)。這具有巨大的潛力,而允許第三方開發(fā)者創(chuàng)建照片編輯擴展則使這一步走得更遠。
制作照片編輯擴展方面有不少復雜的課題,但是它們都不是獨一無二的。創(chuàng)建一個直觀的用戶界面,以及設計圖像處理算法都和圖片編輯擴展一樣充滿了挑戰(zhàn)性,而它們都是一個完整的圖片編輯應用的組成部分。
目前為止有多少用戶留意到這些第三方編輯擴展還有待觀察,但總的來說這有助于提高你應用曝光率。