在 iOS 8 釋出時,蘋果把六種全新擴充套件功能介紹給全世界,它們史無前例的提供了訪問作業系統的可行性。現在,開發者可以利用照片擴充套件來為系統相機應用增加功能。
使用者使用照片編輯擴充套件的流程並不簡單。從選擇編輯的照片開始,需要點選三次才能啟動,其中一步驟是非常小一個按鈕:
然而,這類擴充套件給開發者提供了為使用者創造無縫體驗,建立一致的方法來管理照片的絕佳的機會。
本文在瞭解更詳細的編輯工作流程之前,將先簡單討論如何建立擴充套件以擴充套件的生命週期,我們會通過常見的相關問題和場景來建立照片編輯擴充套件從而得出結論。
本文的示例專案 Filtster 演示瞭如何建立自己的圖片編輯擴充套件。它詮釋使用了數個 Core Image 濾鏡完成簡單的影像過濾效果。完整 Filtster 專案程式碼 可以在 GitHub 上找到。
建立擴充套件
所有擴充套件必須包含在一個功能齊全的 iOS 應用程式之內,照片編輯擴充套件也不例外。這可能意味著你必須做很多令人吃驚的自定義 Core Image 濾鏡的才能讓它到達使用者手中。蘋果如何嚴格審查還有待觀察,因為蘋果商店內的大多數有圖片編輯功能的應用都是在 iOS 8 引入之前就已經存在了的。
為了建立新的圖片編輯擴充套件,需要為已有的 iOS 專案新增新的 target,擴充套件 target 模板如下:
模板由三部分組成:
- Storyboard 圖片編輯擴充套件除了系統在頂部提供的 toolbar 包含取消以及完成按鈕之外,介面幾乎完全可以自定義。
雖然 storyboard 預設不包含 size classes,系統將允許你選擇並啟用該功能,雖然沒有明顯的原因來阻止你使用手動佈局,但蘋果還是強烈建議使用 Auto Layout 來建立照片編輯擴充套件。如果你忽略蘋果的建議你將不得不面對很多潛在風險。
- Info.plist 設定擴充套件的型別和可被接受媒體的型別,以及擴充套件的通用配置。
NSExtension
鍵值是一個字典,它包含擴充套件所需要的配置:NSExtensionPointIdentifier
實體告訴系統這是一個使用com.apple.photo-editing
作為值的照片編輯擴充套件。唯一特殊的 key 是PHSupportedMediaTypes
,它指明可以被操作的媒體型別。在預設情況下,這是一個包含Image
實體的陣列,當然你也可新增Video
選項。 - View Controller 遵守
PHContentEditingController
協議,其中包含了圖片編輯擴充套件需要的生命週期方法。更多詳情見本文下個部分。
值得注意的是不要忘記提供選單內的擴充套件的圖示:
圖示通過宿主 app 的資源目錄內的 App Icon 提供。這裡文件有些讓人迷惑,它暗示你必須在擴充套件本身裡來提供圖示。然而,儘管我們可以提供一個這樣的圖示,但擴充套件將不會使用選擇它。這一點有些爭議,因為蘋果指定與擴充套件相關的圖示必須與容器應用程式的相同。
擴充套件的生命週期
照片編輯擴充套件建立於 Photos 框架之上的,這意味著編輯不是破壞性的。當一個照片資源被編輯的時候,原始檔案始終沒有被修改,編輯的結果將作為副本被儲存下來。另外,語義細節包含了如何重新編輯並儲存調整後的資料。這個資料的意思是編輯可以基於原始檔案重新來過。當你實現圖片編輯擴充套件的時候,你只負責構建你自己的資料物件。
PHAdjustmentData
類含有編輯所需引數,以及兩個格式化的屬性 (formatIdentifier
和 formatVersion
) 用來確定當前編輯擴充套件針對於之前的編輯過的照片的相容性。它們兩個都是字串型別,另外 formatIdentifier
規定為反向域名解析格式。這兩個屬性讓你靈活的建立一套影像編輯的應用程式以及擴充套件,每一種都可以用另一種表示。另外 data
屬性是 NSData
型別。可以被用來按你的需要儲存擴充套件操作的細節,以便讓你的擴充套件能繼續編輯。
開始編輯
當使用者使用你的擴充套件來編輯照片的時候,系統會例項化你的檢視控制器並且初始化照片編輯的生命週期。如果照片之前曾經被編輯,它首先會呼叫 canHandleAdjustmentData(_:)
方法,同時為你提供一個 PHAdjustmentData
物件。因此,你的擴充套件是否可以處理之前編輯過的資料就很重要,這將決定框架傳送的下一個生命週期的方法是什麼。
一旦系統決定提供原始圖片還是之前就被渲染編輯過的圖片,接下來將會呼叫 startContentEditingWithInput(_:, placeholderImage:)
。輸入是一個型別為 PHContentEditingInput
的物件,其中包含了地理位置,建立時間以及媒體型別等來自於原始資源的後設資料,以及你需要編輯的資源細節。除了原始尺寸的輸入圖片的路徑以外,輸入物件還包含一個displaySizedImage
表示相同的圖片資料,但是根據螢幕尺寸進行了適當縮放。這意味著互動編輯可以在較低解析度下進行,以此來確保擴充套件可以保持迅速響應操作並節省能量。
下面是實現方法
1 2 3 4 5 6 7 8 9 10 11 |
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) ... } |
上面的實現中儲存了 contentEditingInput
,因為要完成編輯並從調整後的資料匯入濾鏡引數的時候我們會需要它。
如果你的 canHandleAdjustmentData(_:)
返回 true
,startContentEditingWithInput(_:, placeholderImage:)
將會提供原始圖片,然後你的擴充套件需要根據調整後的資料來重新建立編輯過的圖片。如果這是一個耗時操作,那麼 placeholderImage
將提供一個上次編輯渲染後的臨時圖片來讓你暫時使用。
在這個階段,使用者將通過擴充套件介面的互動來控制編輯的程式。因為擴充套件包含一個檢視控制器,你可以使用任何 UIKit 來實現它。示例專案使用了 Core Image 的濾鏡鏈來完成編輯,所以介面使用了一個自定義的 GLKView
子類來減少 CPU 的負載。
取消編輯
在完成編輯時,使用者可以選擇照片介面提供的取消或者完成按鈕。如果想讓使用者確定是否取消尚未儲存的編輯內容,shouldShowCancelConfirmation
屬性需要重寫並返回 true
:
如果需要取消操作,cancelContentEditing
方法將被呼叫來允許你清空所有臨時資料。
提交修改
一旦使用者決定儲存編輯操作,並且點選了完成按鈕,finishContentEditingWithCompletionHandler(_:)
將會被呼叫。在這個時候,原始尺寸影像需要用與當前顯示的圖片相同設定來編輯,並儲存調整後的資料。
在這時,你可以通過在編輯過程開始時提供的 PHContentEditingInput
物件內的 fullSizeImageURL
來獲取原始尺寸的圖片。
要完成編輯,我們需要呼叫提供的回撥函式,並提供一個從輸入建立的 PHContentEditingOutput
物件。這個輸出物件還包含了一個 renderedContentURL
屬性,用來指定你應該把輸出的 JPEG 資料存放在哪裡:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
func finishContentEditingWithCompletionHandler(completionHandler: ((PHContentEditingOutput!) -> Void)!) { // 在後臺佇列渲染並提供輸出。 dispatch_async(dispatch_get_global_queue(CLong(DISPATCH_QUEUE_PRIORITY_DEFAULT), 0)) { // 從編輯輸入建立編輯輸出。 let output = PHContentEditingOutput(contentEditingInput: self.input) // 提供調整後的資料並且渲染輸出到指定位置。 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) // 呼叫完成回撥提交編輯後的圖片。 completionHandler?(output) } } |
一旦對 completionHandler
返回,你就可以清空臨時資料,並且修改後的檔案已經準備好從擴充套件返回。
常見問題
與建立圖片編輯擴充套件相關的內容其中一些可能有些複雜,本節內容將介紹最重要的幾個。
調整資料 (Adjustment Data)
PHAdjustmentData
是一個只包含三個屬性的簡單類,但是想要用好的話,依然需要遵循一些規則。蘋果建議使用反向域名解析格式來指定 formatIdentifier
,但是 formatVersion
和 data
如何使用將由你自己決定。
重要的是要確保你不同版本圖片編輯擴充套件的相容性,所以我們需要類似語義化版本這樣能提供靈活的管理產品的生命週期的方式。你可以以自己的方式進行解析,也可以依賴於像 SemverKit 之類的第三方框架提供的功能。
最後對於調整資料要說的是 data
本身,它是一個 NSData
資料物件。蘋果提供的唯一建議是它應該用來存放重建編輯時所需要的的設定,而不是編輯本身,這是因為 PHAdjustmentData
物件的尺寸是受 Photo 框架限制的。
對於不是很複雜的擴充套件 (比如 Filtster),這個資料可以是簡單地對一個字典歸檔,程式碼如下:
1 2 3 4 5 6 |
public func encodeFilterParameters() -> NSData { var dataDict = [String : AnyObject]() dataDict["vignetteIntensity"] = vignetteIntensity ... return NSKeyedArchiver.archivedDataWithRootObject(dataDict) } |
接著提供解析方式:
1 2 3 4 5 6 7 8 |
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
類中,同時這個類也負責確定調整資料的相容性:
1 2 3 |
public func supportsFilterIdentifier(identifier: String, version: String) -> Bool return identifier == filterIdentifier && version == filterVersion } |
如果你有更復雜的需求,你可以建立一個自定義的設定類,讓它支援 NSCoding
協議並用類似的方式進行歸檔。
使用者需要可以將互不相容的照片編輯串聯起來 — 如果當前擴充套件無法理解調整資料的話,一張預先渲染好的影像將被作為輸入。比如你可以使用系統的裁剪工具先對圖片進行裁剪,然後再在你的自定義照片編輯擴充套件中使用。在你儲存編輯後的影像時,與之繫結的編輯資料只會包含最近的編輯的細節。你可以將之前的不相容的編輯的調整資料儲存到你的輸出調整資料中,這樣你就可以為濾鏡鏈中你的階段實現還原功能。Photo 框架提供的還原功能將移除所有編輯,並把照片恢復到原始狀態:
程式碼/資料共享
照片編輯擴充套件作為一個嵌入式二進位制檔案包含在容器應用中。因為蘋果要求這個容器應用必須有完整功能,因此你建立的照片編輯擴充套件,很可能與容器應用有相同的功能。你可能會希望在應用擴充套件和容器之間共享程式碼和資料。
共享程式碼通過 iOS 8 新功能 — 建立 Cocoa Touch 框架 target 來實現。你可以向其中新增共用的功能,例如濾鏡鏈和自定義檢視類,並在應用和擴充套件中同時使用。
值得注意的是因為用於建立擴充套件,你必須在 Target 設定介面將 API 相容性限制為僅擴充套件可用:
共享資料的需求明顯要少很多,在許多情況下並不存在。然而如果需要,你可以通過把應用和擴充套件都新增到一個關聯到你的開發者賬號的 app group 中的方式,來建立一個共享容器 (shared container)。共享容器代表的是磁碟上的一塊共享的空間,你可以使用任何你喜歡的方式使用它,比如 NSUserDefaults
,SQLite
或者寫檔案。
除錯與分析
Xcode 除錯雖然有一些潛在癥結,但已經相當友好了。選擇擴充套件的 scheme 並編譯執行,接著會詢問你希望啟動哪一個應用,因為圖片編輯擴充套件只能在系統照片應用中實現,所以你應該選擇照片應用:
如果這麼做啟動的是你的容器應用的話,你可以編輯擴充套件 scheme 設定 executable 為 Ask on Launch 來解決。
Xcode 然後會等待你開啟你的照片編輯擴充套件,然後將偵錯程式掛載上去。從這時開始,你就可以用除錯標準 iOS 應用的方式來除錯擴充套件了。將偵錯程式附加到擴充套件可能需要一些時間,所以當你啟用擴充套件時,擴充套件可能會失去響應一段時間。如果你想評估啟動時間的話,可以在 release 模式下執行它。
效能分析和除錯類似,分析器在擴充套件開始執行後附加上去。你可以更新擴充套件相關 scheme 指定 Xcode 詢問應該啟動哪一個應用來執行分析。
記憶體限制
擴充套件不是一個全功能 iOS 應用,因此訪問系統資源時要受到限制。更特別的是,如果使用者使用太多記憶體,系統將優先關閉擴充套件程式。我們無法確定具體的記憶體限制,因為記憶體管理是由 iOS 內部處理的,但有這肯定是基於像是裝置,宿主應用,以及其他應用程式的記憶體壓力這些因素的。所以其實並沒有硬性的限制,但我們還是應該儘量減少記憶體佔用。
圖片處理是一個高記憶體操作,特別是處理的物件是來自 iPhone 相機的高清晰度圖片。你需要做幾件事情來確保照片編輯擴充套件的記憶體使用量降到最低。
- 使用顯示尺寸的圖片 當你開始編輯程式時,系統提供了一張適合螢幕尺寸的圖片。用它來替代原始圖片,將在互動編輯階段將顯著減少記憶體使用。
- 限制 Core Graphics 上下文數量 雖然使用 Core Graphics 來處理圖片是正確的方式,但不要忘記 Core Graphics 上下文其實是一大塊記憶體。如果你需要上下文,那麼需要保持數量到最低。儘可能的重用,並想想你是否以最佳的方式在使用它。
- 使用 GPU 無論通過 Core Image 還是類似 GPUImage 的第三方框架來用 GPU 進行處理,你可以通過鏈式呼叫濾鏡來降低記憶體並且消除中間快取區需求。
因為影像編輯本身肯定就需要高記憶體,所以與其他擴充套件相比,照片編輯擴充套件的可用記憶體要多那麼一些。在 ad hoc 測試中,圖片編輯擴充套件可以使用高於 100 MB 記憶體。鑑於來自 800 萬畫素相機的照片大約 22MB,所以這個記憶體量對於大多數圖片編輯擴充套件來說是夠用的。
結論
iOS 8 之前,第三方開發者無法在他自己應用程式之外向使用者提供功能。擴充套件的出現徹底改變這一狀況,特別是照片編輯擴充套件允許你把程式碼執行於照片應用核心中。儘管多次點選的流程略顯複雜,但照片編輯擴充套件使用 Photo 框架的功能提供了連貫和整合的使用者體驗。
可恢復的編輯一直是像 Aperture 或 Lightroom 這樣的桌面應用的殺手級功能。而現在在 iOS 中使用 Photo 框架,也可以為這個功能建立一個通用架構。這具有巨大的潛力,而允許第三方開發者建立照片編輯擴充套件則使這一步走得更遠。
製作照片編輯擴充套件方面有不少複雜的課題,但是它們都不是獨一無二的。建立一個直觀的使用者介面,以及設計影像處理演算法都和圖片編輯擴充套件一樣充滿了挑戰性,而它們都是一個完整的圖片編輯應用的組成部分。
目前為止有多少使用者留意到這些第三方編輯擴充套件還有待觀察,但總的來說這有助於提高你應用曝光率。