鍍金池/ 教程/ iOS/ Swift 的函數(shù)式 API
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開(kāi)發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見(jiàn)的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺(jué)
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開(kāi)發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

Swift 的函數(shù)式 API

在過(guò)去的時(shí)間里,人們對(duì)于設(shè)計(jì) API 總結(jié)了很多通用的模式和最佳實(shí)踐方案。一般情況下,我們總是可以從蘋果的 Foundation、Cocoa、Cocoa Touch 和很多其他框架中總結(jié)出一些開(kāi)發(fā)中的范例。毫無(wú)疑問(wèn),對(duì)于“特定情境下的 API 應(yīng)該如何設(shè)計(jì)”這個(gè)問(wèn)題,不同的人總是有著不同的意見(jiàn),對(duì)于這個(gè)問(wèn)題有很大的討論空間。不過(guò)對(duì)于很多 Objective-C 的開(kāi)發(fā)者來(lái)說(shuō),對(duì)于那些常用的模式早已習(xí)以為常。

隨著 Swift 的出現(xiàn),設(shè)計(jì) API 引起了更多的問(wèn)題。絕大多數(shù)情況下,我們只能繼續(xù)做著手頭的工作,然后把現(xiàn)有的方法翻譯成 Swift 版本。不過(guò),這對(duì)于 Swift 來(lái)說(shuō)并不公平,因?yàn)楹?Objective-C 相比,Swift 添加了很多新的特性。引用 Swift 創(chuàng)始人 Chris Lattner 的一段話:

Swift 引入了泛型和函數(shù)式編程的思想,極大地?cái)U(kuò)展了設(shè)計(jì)的空間。

在這篇文章里,我們將會(huì)圍繞 Core Image 進(jìn)行 API 封裝,以此為例,探索如何在 API 設(shè)計(jì)中使用這些新的工具。 Core Image 是一個(gè)功能強(qiáng)大的圖像處理框架,但是它的 API 有時(shí)有點(diǎn)笨重。 Core Image 的 API 是弱類型的 - 它通過(guò)鍵值對(duì) (key-value) 設(shè)置圖像濾鏡。這樣在設(shè)置參數(shù)的類型和名字時(shí)很容易失誤,會(huì)導(dǎo)致運(yùn)行時(shí)錯(cuò)誤。新的 API 將會(huì)十分的安全和模塊化,通過(guò)使用類型而不是鍵值對(duì)來(lái)規(guī)避這樣的運(yùn)行時(shí)錯(cuò)誤。

目標(biāo)

我們的目標(biāo)是構(gòu)建一個(gè) API ,讓我們可以簡(jiǎn)單安全的組裝自定義濾鏡。舉個(gè)例子,在文章的結(jié)尾,我們可以這樣寫:

let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)

上面構(gòu)建了一個(gè)自定義的濾鏡,先模糊圖像,然后再添加一個(gè)顏色蒙版。為了達(dá)到這個(gè)目標(biāo),我們將充分利用 Swift 函數(shù)是一等公民這一特性。項(xiàng)目源碼可以在 Github 上的這個(gè)示例項(xiàng)目中下載。

Filter 類型

CIFilterCore Image 中的一個(gè)核心類,用來(lái)創(chuàng)建圖像濾鏡。當(dāng)實(shí)例化一個(gè) CIFilter 對(duì)象之后,你 (幾乎) 總是通過(guò) kCIInputImageKey 來(lái)輸入圖像,然后通過(guò) kCIOutputImageKey 獲取返回的圖像,返回的結(jié)果可以作為下一個(gè)濾鏡的參數(shù)輸入。

在我們即將開(kāi)發(fā)的 API 里,我們會(huì)把這些鍵值對(duì) (key-value) 對(duì)應(yīng)的真實(shí)內(nèi)容抽離出來(lái),為用戶提供一個(gè)安全的強(qiáng)類型 API。我們定義了自己的濾鏡類型 Filter,它是一個(gè)可以傳入圖片作為參數(shù)的函數(shù),并且返回一個(gè)新的圖片。

typealias Filter = CIImage -> CIImage

這里我們用 typealias 關(guān)鍵字,為 CIImage -> CIImage類型定義了我們自己的名字,這個(gè)類型是一個(gè)函數(shù),它的參數(shù)是一個(gè) CIImage ,返回值也是 CIImage 。這是我們后面開(kāi)發(fā)需要的基礎(chǔ)類型。

如果你不太熟悉函數(shù)式編程,你可能對(duì)于把一個(gè)函數(shù)類型命名為 Filter 感覺(jué)有點(diǎn)奇怪,通常來(lái)說(shuō),我們會(huì)用這樣的命名來(lái)定義一個(gè)類。如果我們很想以某種方式來(lái)表現(xiàn)這個(gè)類型的函數(shù)式的特性,我們可以把它命名成 FilterFunction 或者一些其他的類似的名字。但是,我們有意識(shí)的選擇了 Filter 這個(gè)名字,因?yàn)樵诤瘮?shù)式編程的核心哲學(xué)里,函數(shù)就是值,函數(shù)和結(jié)構(gòu)體、整數(shù)、多元組、或者類,并沒(méi)有任何區(qū)別。一開(kāi)始我也不是很適應(yīng),不過(guò)一段時(shí)間之后發(fā)現(xiàn),這樣做確實(shí)很有意義。

構(gòu)建濾鏡

現(xiàn)在我們已經(jīng)定義了 Filter 類型,接下來(lái)可以定義函數(shù)來(lái)構(gòu)建特定的濾鏡了。這些函數(shù)需要參數(shù)來(lái)設(shè)置特定的濾鏡,并且返回一個(gè)類型為 Filter 的值。這些函數(shù)大概是這個(gè)樣子:

func myFilter(/* parameters */) -> Filter

注意返回的值 Filter 本身就是一個(gè)函數(shù),在后面有利于我們將多個(gè)濾鏡組合起來(lái),以達(dá)到理想的處理效果。

為了讓后面的開(kāi)發(fā)更輕松一點(diǎn),我們擴(kuò)展了 CIFilter 類,添加了一個(gè) convenience 的初始化方法,以及一個(gè)用來(lái)獲取輸出圖像的計(jì)算屬性:

typealias Parameters = Dictionary<String, AnyObject>

extension CIFilter {

    convenience init(name: String, parameters: Parameters) {
        self.init(name: name)
        setDefaults()
        for (key, value : AnyObject) in parameters {
            setValue(value, forKey: key)
        }
    }

    var outputImage: CIImage { return self.valueForKey(kCIOutputImageKey) as CIImage }

}

這個(gè) convenience 初始化方法有兩個(gè)參數(shù),第一個(gè)參數(shù)是濾鏡的名字,第二個(gè)參數(shù)是一個(gè)字典。字典中的鍵值對(duì)將會(huì)被設(shè)置成新濾鏡的參數(shù)。我們 convenience 初始化方法先調(diào)用了指定的初始化方法,這符合 Swift 的開(kāi)發(fā)規(guī)范。

計(jì)算屬性 outputImage 可以方便地從濾鏡對(duì)象中獲取到輸出的圖像。它查找 kCIOutputImageKey 對(duì)應(yīng)的值并且將其轉(zhuǎn)換成一個(gè) CIImage 對(duì)象。通過(guò)提供這個(gè)屬性, API 的用戶不再需要對(duì)返回的結(jié)果手動(dòng)進(jìn)行類型轉(zhuǎn)換了。

模糊

有了這些東西,現(xiàn)在我們就可以定義屬于自己的簡(jiǎn)單濾鏡了。高斯模糊濾鏡只需要一個(gè)模糊半徑作為參數(shù),我們可以非常容易的完成一個(gè)模糊濾鏡:

func blur(radius: Double) -> Filter {
    return { image in
        let parameters : Parameters = [kCIInputRadiusKey: radius, kCIInputImageKey: image]
        let filter = CIFilter(name:"CIGaussianBlur", parameters:parameters)
        return filter.outputImage
    }
}

就是這么簡(jiǎn)單,這個(gè)模糊函數(shù)返回了一個(gè)函數(shù),新的函數(shù)的參數(shù)是一個(gè)類型為 CIImage 的圖片,返回值 (filter.outputImage) 是一個(gè)新的圖片 。這個(gè)模糊函數(shù)的格式是 CIImage -> CIImage ,滿足我們前面定義的 Filter 類型的格式。

這個(gè)例子只是對(duì) Core Image 中已有濾鏡的一個(gè)簡(jiǎn)單的封裝,我們可以多次重復(fù)同樣的模式,創(chuàng)建屬于我們自己的濾鏡函數(shù)。

顏色蒙版

現(xiàn)在讓我們定義一個(gè)顏色濾鏡,可以在現(xiàn)有的圖片上面加上一層顏色蒙版。 Core Image 默認(rèn)沒(méi)有提供這個(gè)濾鏡,不過(guò)我們可以通過(guò)已有的濾鏡組裝一個(gè)。

我們使用兩個(gè)模塊來(lái)完成這個(gè)工作,一個(gè)是顏色生成濾鏡 (CIConstantColorGenerator),另一個(gè)是資源合成濾鏡 (CISourceOverCompositing)。讓我們先定義一個(gè)生成一個(gè)常量顏色面板的濾鏡:

func colorGenerator(color: UIColor) -> Filter {
    return { _ in
        let filter = CIFilter(name:"CIConstantColorGenerator", parameters: [kCIInputColorKey: color])
        return filter.outputImage
    }
}

這段代碼看起來(lái)和前面的模糊濾鏡差不多,不過(guò)有一個(gè)較為明顯的差異:顏色生成濾鏡不會(huì)檢測(cè)輸入的圖片。所以在函數(shù)里我們不需要給傳入的圖片參數(shù)命名,我們使用了一個(gè)匿名參數(shù) _ 來(lái)強(qiáng)調(diào)這個(gè) filter 的圖片參數(shù)是被忽略的。

接下來(lái),我們來(lái)定義合成濾鏡:

func compositeSourceOver(overlay: CIImage) -> Filter {
    return { image in
        let parameters : Parameters = [ 
            kCIInputBackgroundImageKey: image, 
            kCIInputImageKey: overlay
        ]
        let filter = CIFilter(name:"CISourceOverCompositing", parameters: parameters)
        return filter.outputImage.imageByCroppingToRect(image.extent())
    }
}

在這里我們將輸出圖像裁剪到和輸入大小一樣。這并不是嚴(yán)格需要的,要取決于我們想讓濾鏡如何工作。不過(guò),在后面我們的例子中我們可以看出來(lái)這是一個(gè)明智之舉。

func colorOverlay(color: UIColor) -> Filter {
    return { image in
        let overlay = colorGenerator(color)(image)
        return compositeSourceOver(overlay)(image)
    }
}

我們?cè)僖淮畏祷亓艘粋€(gè)參數(shù)為圖片的函數(shù),colorOverlay 在一開(kāi)始先調(diào)用了 colorGenerator 濾鏡。colorGenerator 濾鏡需要一個(gè)顏色作為參數(shù),并且返回一個(gè)濾鏡。因此 colorGenerator(color)Filter 類型的。但是 Filter 類型本身是一個(gè) CIImageCIImage 轉(zhuǎn)換的函數(shù),我們可以在 colorGenerator(color) 后面加上一個(gè)類型為 CIImage 的參數(shù),這樣可以得到一個(gè)類型為 CIImage 的蒙版圖片。這就是在定義 overlay 的時(shí)候發(fā)生的事情:我們用 colorGenerator 函數(shù)創(chuàng)建了一個(gè)濾鏡,然后把圖片作為一個(gè)參數(shù)傳給了這個(gè)濾鏡,從而得到了一張新的圖片。返回值 compositeSourceOver(overlay)(image) 和這個(gè)基本相似,它由一個(gè)濾鏡 compositeSourceOver(overlay) 和一個(gè)圖片參數(shù) image 組成。

組合濾鏡

現(xiàn)在我們已經(jīng)定義了一個(gè)模糊濾鏡和一個(gè)顏色濾鏡,我們?cè)谑褂玫臅r(shí)候可以把它們組合在一起:我們先將圖片做模糊處理,然后再在上面放一個(gè)紅色的蒙層。讓我們先加載一張圖片:

let url = NSURL(string: "http://tinyurl.com/m74sldb");
let image = CIImage(contentsOfURL: url)

現(xiàn)在我們可以把濾鏡組合起來(lái),同時(shí)應(yīng)用到一張圖片上:

let blurRadius = 5.0
let overlayColor = UIColor.redColor().colorWithAlphaComponent(0.2)
let blurredImage = blur(blurRadius)(image)
let overlaidImage = colorOverlay(overlayColor)(blurredImage)

我們又一次的通過(guò)濾鏡組裝了圖片。比如在倒數(shù)第二行,我們先得到了模糊濾鏡 blur(blurRadius) ,然后再把這個(gè)濾鏡應(yīng)用到圖片上。

函數(shù)組裝

不過(guò),我們可以做的比上面的更好。我們可以簡(jiǎn)單的把兩行濾鏡的調(diào)用組合在一起變成一行,這是我腦海中想到的第一個(gè)能改進(jìn)的地方:

let result = colorOverlay(overlayColor)(blur(blurRadius)(image))

不過(guò),這些圓括號(hào)讓這行代碼完全不具有可讀性,更好的方式是定義一個(gè)函數(shù)來(lái)完成這項(xiàng)任務(wù):

func composeFilters(filter1: Filter, filter2: Filter) -> Filter {
    return { img in filter2(filter1(img)) }
}

composeFilters 函數(shù)的兩個(gè)參數(shù)都是 Filter ,并且返回了一個(gè)新的 Filter 濾鏡。組裝后的濾鏡需要一個(gè) CIImage 類型的參數(shù),并且會(huì)把這個(gè)參數(shù)分別傳給 filter1filter2 。現(xiàn)在我們可以用 composeFilters 來(lái)定義我們自己的組合濾鏡:

let myFilter = composeFilters(blur(blurRadius), colorOverlay(overlayColor))
let result = myFilter(image)

我們還可以更進(jìn)一步的定義一個(gè)濾鏡運(yùn)算符,讓代碼更具有可讀性,

infix operator >|> { associativity left }

func >|> (filter1: Filter, filter2: Filter) -> Filter {
    return { img in filter2(filter1(img)) }
}

運(yùn)算符通過(guò) infix 關(guān)鍵字定義,表明運(yùn)算符具有 兩個(gè)參數(shù)。associativity left 表明這個(gè)運(yùn)算滿足左結(jié)合律,即:f1 >|> f2 >|> f3 等價(jià)于 (f1 >|> f2) >|> f3。通過(guò)使這個(gè)運(yùn)算滿足左結(jié)合律,再加上運(yùn)算內(nèi)先應(yīng)用了左側(cè)的濾鏡,所以在使用的時(shí)候?yàn)V鏡順序是從左往右的,就像 Unix 管道一樣。

剩余的部分是一個(gè)函數(shù),內(nèi)容和 composeFilters 基本相同,只不過(guò)函數(shù)名變成了 >|>。

接下來(lái)我們把這個(gè)組合濾鏡運(yùn)算器應(yīng)用到前面的例子中:

let myFilter = blur(blurRadius) >|> colorOverlay(overlayColor)
let result = myFilter(image)

運(yùn)算符讓代碼變得更易于閱讀和理解濾鏡使用的順序,調(diào)用濾鏡的時(shí)候也更加的方便。就好比是 1 + 2 + 3 + 4 要比 add(add(add(1, 2), 3), 4) 更加清晰,更加容易理解。

自定義運(yùn)算符

很多 Objective-C 的開(kāi)發(fā)者對(duì)于自定義運(yùn)算符持有懷疑態(tài)度。在 Swift 剛發(fā)布的時(shí)候,這是一個(gè)并沒(méi)有很受歡迎的特性。很多人在 C++ 中遭遇過(guò)自定義運(yùn)算符過(guò)度使用 (甚至濫用) 的情況,有些是個(gè)人經(jīng)歷過(guò)的,有些是聽(tīng)到別人談起的。

你可能對(duì)于前面定義的運(yùn)算符 >|> 持有同樣的懷疑態(tài)度,畢竟如果每個(gè)人都定義自己的運(yùn)算符,那代碼豈不是很難理解了?值得慶幸的是在函數(shù)式編程里有很多的操作,為這些操作定義一個(gè)運(yùn)算符并不是一件很罕見(jiàn)的事情。

我們定義的濾鏡組合運(yùn)算符是一個(gè)函數(shù)組合的例子,這是一個(gè)在函數(shù)式編程中廣泛使用的概念。在數(shù)學(xué)里,兩個(gè)函數(shù) fg 的組合有時(shí)候?qū)懽?f ° g,這樣定義了一種全新的函數(shù),將輸入的 x 映射到 f(g(x)) 上。這恰好就是我們的 >|> 所做的工作 (除了函數(shù)的逆向調(diào)用)。

泛型

仔細(xì)想想,其實(shí)我們并沒(méi)有必要去定義一個(gè)用來(lái)專門組裝濾鏡的運(yùn)算符,我們可以用一個(gè)泛型的運(yùn)算符來(lái)組裝函數(shù)。目前我們的 >|> 是這樣的:

func >|> (filter1: Filter, filter2: Filter) -> Filter

這樣定義之后,我們傳入的參數(shù)只能是 Filter 類型的濾鏡。

但是,我們可以利用 Swift 的通用特性來(lái)定義一個(gè)泛型的函數(shù)組合運(yùn)算符:

func >|> <A, B, C>(lhs: A -> B, rhs: B -> C) -> A -> C {
    return { x in rhs(lhs(x)) }
}

這個(gè)一開(kāi)始可能很難理解 -- 至少對(duì)我來(lái)說(shuō)是這樣。但是分開(kāi)的看了各個(gè)部分之后,一切都變得清晰起來(lái)。

首先,我們來(lái)看一下函數(shù)名后面的尖括號(hào)。尖括號(hào)定義了這個(gè)函數(shù)適用的泛型類型。在這個(gè)例子里我們定義了三個(gè)類型:A、B 和 C。因?yàn)槲覀儾](méi)有指定這些類型,所以它們可以代表任何東西。

接下來(lái)讓我們來(lái)看看函數(shù)的參數(shù):第一個(gè)參數(shù):lhs (left-hand side 的縮寫),是一個(gè)類型為 A -> B 的函數(shù)。這代表一個(gè)函數(shù)的參數(shù)為 A,返回值的類型為 B。第二個(gè)參數(shù):rhs (right-hand side 的縮寫),是一個(gè)類型為 B -> C 的函數(shù)。參數(shù)命名為 lhs 和 rhs,因?yàn)樗鼈兎謩e對(duì)應(yīng)操作符左邊和右邊的值。

重寫了沒(méi)有 Filter 的濾鏡組合運(yùn)算符之后,我們很快就發(fā)現(xiàn)其實(shí)前面實(shí)現(xiàn)的組合運(yùn)算符只是泛型函數(shù)中的一個(gè)特殊情況:

func >|> (filter1: CIImage -> CIImage, filter2: CIImage -> CIImage) -> CIImage -> CIImage

把我們腦海中的泛型類型 A、B、C 都換成 CIImage,這樣可以清晰的理解用通用運(yùn)算符的來(lái)替換濾鏡組合運(yùn)算符是多么的有用。

結(jié)論

至此,我們成功的用函數(shù)式 API 封裝了 Core Image。希望這個(gè)例子能夠很好的說(shuō)明,對(duì)于 Objective-C 的開(kāi)發(fā)者來(lái)說(shuō),在我們所熟知的 API 的設(shè)計(jì)模式之外有一片完全不同的世界。有了 Swift,我們現(xiàn)在可以動(dòng)手探索那些全新的領(lǐng)域,并且將它們充分地利用起來(lái)。