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