在 Swift 2.0 當中使用 C 語言回調(diào)

作者:Ole Begemann释漆,原文鏈接悲没,原文日期:2015-06-22
譯者:小鍋;校對:shanks;定稿:shanks

更新:

  • 2015-06-25
    增加關于傳遞另一個(可以捕獲外部變量的)閉包到 userInfo 參數(shù)的備注示姿。

  • 2015-07-01
    針對 Xcode 7 beta 2 更新從 CGPathElement 創(chuàng)建一個 PathElement 類型的代碼甜橱。

幾年前,我曾經(jīng)寫過一篇關于如何獲取 CGPathUIBezierPath 中元素的文章栈戳∑癜粒可以通過調(diào)用 CGPathApply 函數(shù),并給這個函數(shù)傳入一個回調(diào)的函數(shù)指針來達到這個目的子檀。 隨后 CGPathApply 會對 path(CGPath 或 UIBezierPath) 中的每一個元素調(diào)用這個回調(diào)函數(shù)镊掖。

很不幸,我們無法在 Swift 1.x 中做到這件事褂痰,因為我們沒辦法將 Swift 函數(shù)橋接到 C 語言函數(shù)亩进。我們需要使用 C 或者 Objective-C 寫一個小小的包裝層來對這個回調(diào)函數(shù)進行封裝。

而在 Swift 2 當中缩歪,可以直接使用原生的 Swift 來完成這件事归薛。Swift 將 C 語言的函數(shù)指針作為閉包來導入。在任何需要傳入 C 語言函數(shù)指針的地方匪蝙,我們都可以傳入與該函數(shù)指針參數(shù)相匹配的 Swift 閉包或者函數(shù) —— 除了一個特殊情況:與閉包不同的是主籍,C 語言的函數(shù)指針沒有捕獲狀態(tài)(capturing state)的概念伯病。因此钙畔,編譯器只允許傳入不捕獲任何外部變量的 Swift 閉包來對C語言函數(shù)指針進行橋接。Swift 使用了新的 @convention(c) 注解來標識這一約定匾委。

下載本篇文章的playground需忿,要求 Swift 2/Xcode 7诅炉。

獲取 UIBezierPath 中的元素

讓我們使用迭代一個 path 中元素這個熟悉的任務來作為例子。

一個 Swift 化后的數(shù)據(jù)結(jié)構(gòu)

首先屋厘,考慮一下我們必須處理的數(shù)據(jù)結(jié)構(gòu)涕烧。CGPathApply 會將一個 CGPathElement 的指針傳遞給回調(diào)函數(shù)(或者閉包)。CGPathElement 是一個結(jié)構(gòu)體汗洒,這個結(jié)構(gòu)體包含了一個標識 path 元素類型的的常量议纯,以及一個 CGPoint 類型的C語言數(shù)組。這個數(shù)組中的點(point)的個數(shù)將在 0 到 3 之間溢谤,取決于元素的類型瞻凤。

在 Swift 當中直接使用 CGPathElement 很不方便。C語言數(shù)組在 Swift 中是被當作 UnsafeMutablePointer<CGPoint> 來導入的世杀,并且它的生命周期被限制在該回調(diào)函數(shù)中阀参,因此,如果想在別的地方使用這個數(shù)組瞻坝,我們就得將它的內(nèi)容復制并保存蛛壳。更進一步地,如果有一個更安全的方式來獲取每個元素中點(point)的個數(shù)就更好了。

一個關聯(lián)了點(point)個數(shù)的 Swift 枚舉衙荐,會是達到這個目的的理想類型捞挥。我們同時還要定義一個從 CGPathElement 轉(zhuǎn)換的自定義構(gòu)造器。

/// A Swiftified representation of a `CGPathElement`
///
/// Simpler and safer than `CGPathElement` because it doesn’t use a
/// C array for the associated points.
public enum PathElement {
    case MoveToPoint(CGPoint)
    case AddLineToPoint(CGPoint)
    case AddQuadCurveToPoint(CGPoint, CGPoint)
    case AddCurveToPoint(CGPoint, CGPoint, CGPoint)
    case CloseSubpath

    init(element: CGPathElement) {
        switch element.type {
        case .MoveToPoint:
            self = .MoveToPoint(element.points[0])
        case .AddLineToPoint:
            self = .AddLineToPoint(element.points[0])
        case .AddQuadCurveToPoint:
            self = .AddQuadCurveToPoint(element.points[0], element.points[1])
        case .AddCurveToPoint:
            self = .AddCurveToPoint(element.points[0], element.points[1], element.points[2])
        case .CloseSubpath:
            self = .CloseSubpath
        }
    }
}

接下來忧吟,為我們的新數(shù)據(jù)類型定義一個格式化的輸出砌函,這將使我們調(diào)試時更加方便:

extension PathElement : CustomDebugStringConvertible {
    public var debugDescription: String {
        switch self {
        case let .MoveToPoint(point):
            return "\(point.x) \(point.y) moveto"
        case let .AddLineToPoint(point):
            return "\(point.x) \(point.y) lineto"
        case let .AddQuadCurveToPoint(point1, point2):
            return "\(point1.x) \(point1.y) \(point2.x) \(point2.y) quadcurveto"
        case let .AddCurveToPoint(point1, point2, point3):
            return "\(point1.x) \(point1.y) \(point2.x) \(point2.y) \(point3.x) \(point3.y) curveto"
        case .CloseSubpath:
            return "closepath"
        }
    }
}

再接再厲,來將 PathElement 實現(xiàn)為可比較的(Equatable)(因為我們始終應該這樣做

extension PathElement : Equatable { }

public func ==(lhs: PathElement, rhs: PathElement) -> Bool {
    switch(lhs, rhs) {
    case let (.MoveToPoint(l), .MoveToPoint(r)):
        return l == r
    case let (.AddLineToPoint(l), .AddLineToPoint(r)):
        return l == r
    case let (.AddQuadCurveToPoint(l1, l2), .AddQuadCurveToPoint(r1, r2)):
        return l1 == r1 && l2 == r2
    case let (.AddCurveToPoint(l1, l2, l3), .AddCurveToPoint(r1, r2, r3)):
        return l1 == r1 && l2 == r2 && l3 == r3
    case (.CloseSubpath, .CloseSubpath):
        return true
    case (_, _):
        return false
    }
}

枚舉 Path 元素

現(xiàn)在到了有趣的部分了溜族。我們要對 UIBezierPath 增加一個名為 elements 的計算屬性讹俊,它會迭代 path 并且返回一個 PathElement 類型的數(shù)組。我們需要調(diào)用 CGPathApply() 并傳遞給它一個閉包參數(shù)斩祭,它會對每個元素都調(diào)用這個閉包劣像。在這個閉包內(nèi)部乡话,我們需要將 CGPathElement 轉(zhuǎn)化為 PathElement 并將它存儲在一個數(shù)組當中摧玫。 最后一部分的實現(xiàn)并不像聽起來的那么簡單,因為 C 函數(shù)指針的調(diào)用約定不允許我們對外部上下文中的變量進行捕獲绑青。

這個 API 的純 C 實現(xiàn)也面臨著同樣的問題诬像,因此 CGPathApply 接收了一個額外的 void * 類型的參數(shù)并將這個指針傳遞給回調(diào)函數(shù)。這使得調(diào)用者可以傳遞一個任意類型的數(shù)據(jù)(比如一個指向數(shù)組的指針)給回調(diào)函數(shù) —— 這正是我們所需要的闸婴。

void * 類型在 Swift 當中是被作為 UnsafeMutablePointer<Void> 引入的坏挠。我們先創(chuàng)建一個 Swift 數(shù)組用于存儲 PathElement 的值,然后使用 withUnsafeMutablePointer() 來獲得指向這個數(shù)組的指針邪乍,這個指針會作為參數(shù)傳遞到該函數(shù)的閉包中降狠。在該閉包當中,我們就可以開始調(diào)用 CGPathApply庇楞。在 CGPathApply 的內(nèi)部閉包中最后一步是要將 void 指針轉(zhuǎn)型回 UnsafeMutablePointer<[PathElement]>榜配,并通過 memory 屬性來直接獲取底層的數(shù)組。(注:我不是很確定這是不是將一個數(shù)組傳遞到閉包中的最好方法吕晌,如果你知道有更好的方法蛋褥,請讓我知道)

完整的實現(xiàn)看起來是這樣子的:

extension UIBezierPath {
    var elements: [PathElement] {
        var pathElements = [PathElement]()
        withUnsafeMutablePointer(&pathElements) { elementsPointer in
            CGPathApply(CGPath, elementsPointer) { (userInfo, nextElementPointer) in
                let nextElement = PathElement(element: nextElementPointer.memory)
                let elementsPointer = UnsafeMutablePointer<[PathElement]>(userInfo)
                elementsPointer.memory.append(nextElement)
            }
        }
        return pathElements
    }
}

更新:在蘋果開發(fā)者論壇中的一個帖子里,蘋果員工 Quinn "The Eskimo!" 提出了一個稍微不同的方法:我們可以傳遞指向另一個閉包的指針給 userInfo 參數(shù)睛驳,而非我們想要操作的數(shù)組的指針烙心。因為這個閉包沒有被C調(diào)用約定所限制,因此它是可以捕獲外部變量的乏沸。

創(chuàng)建一個閉包的指針會涉及到丑陋的 @convention(block) 注解和 unsafeBitCast 魔法(或者是將閉包包裝到一個包裝類型中)淫茵,我不太確定我是否會喜歡這種形式。不過使用這種方法確實是相當方便的蹬跃。

收尾

現(xiàn)在痘昌,我們有了一個包含 path 元素的數(shù)組,很自然地,我們會想要將 UIBezierPath 轉(zhuǎn)化成一個序列辆苔。這使得用戶可以使用 for-in 循環(huán)來對 path 進行迭代算灸,或者直接對它調(diào)用 mapfilter 方法。

extension UIBezierPath : SequenceType {
    public func generate() -> AnyGenerator<PathElement> {
        return anyGenerator(elements.generate())
    }
}

最后驻啤,這是一個便于 UIBezierPath 調(diào)試的格式化輸出的實現(xiàn)菲驴,這個實現(xiàn)參考了 OS X 上的 NSBezierPath 的輸出格式。

extension UIBezierPath : CustomDebugStringConvertible {
    public override var debugDescription: String {
        let cgPath = self.CGPath;
        let bounds = CGPathGetPathBoundingBox(cgPath);
        let controlPointBounds = CGPathGetBoundingBox(cgPath);

        let description = "\(self.dynamicType)\n"
            + "    Bounds: \(bounds)\n"
            + "    Control Point Bounds: \(controlPointBounds)"
            + elements.reduce("", combine: { (acc, element) in
                acc + "\n    \(String(reflecting: element))"
            })
        return description
    }
}

現(xiàn)在用一個示例 path 來進行一下試驗:

let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
path.addLineToPoint(CGPoint(x: 100, y: 0))
path.addLineToPoint(CGPoint(x: 50, y: 100))
path.closePath()
path.moveToPoint(CGPoint(x: 0, y: 100))
path.addQuadCurveToPoint(CGPoint(x: 100, y: 100),
    controlPoint: CGPoint(x: 50, y: 200))
path.closePath()
path.moveToPoint(CGPoint(x: 100, y: 0))
path.addCurveToPoint(CGPoint(x: 200, y: 0),
    controlPoint1: CGPoint(x: 125, y: 100),
    controlPoint2: CGPoint(x: 175, y: -100))
path.closePath()
The example path
The example path

也可以迭代 path 中的每一個元素骑冗,然后打印出每個元素的描述(description)字符串:

for element in path {
    debugPrint(element)
}

/* Output:
0.0 0.0 moveto
100.0 0.0 lineto
50.0 100.0 lineto
closepath
0.0 100.0 moveto
50.0 200.0 100.0 100.0 quadcurveto
closepath
100.0 0.0 moveto
125.0 100.0 175.0 -100.0 200.0 0.0 curveto
closepath
*/

或者赊瞬,我們也可以計算 path 中的閉合路徑(closepath)的個數(shù):

let closePathCount = path.filter {
        element in element == PathElement.CloseSubpath
    }.count
// -> 3

總結(jié)

Swift 2 中自動地將 C 語言函數(shù)指針橋接到為閉包。這使得對大量的接收函數(shù)指針的 C 語言API 進行操作成為可能(并且相當方便)贼涩。因為 C 語言的調(diào)用約定巧涧,這種類型的閉包無法捕獲外部的狀態(tài),所以我們經(jīng)常需要將回調(diào)閉包中需要用到的數(shù)據(jù)通過一個外部的 void 類型的指針傳入遥倦,而這正是很多基于C語言的 API 的做法谤绳。在 Swift 當中進行這樣的操作會有點繞,不過卻是完全可能的袒哥。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缩筛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子堡称,更是在濱河造成了極大的恐慌瞎抛,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件却紧,死亡現(xiàn)場離奇詭異桐臊,居然都是意外死亡,警方通過查閱死者的電腦和手機晓殊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門断凶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挺物,你說我怎么就攤上這事懒浮。” “怎么了识藤?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵砚著,是天一觀的道長。 經(jīng)常有香客問我痴昧,道長稽穆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任赶撰,我火速辦了婚禮舌镶,結(jié)果婚禮上柱彻,老公的妹妹穿的比我還像新娘。我一直安慰自己餐胀,他們只是感情好哟楷,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著否灾,像睡著了一般卖擅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上墨技,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天惩阶,我揣著相機與錄音,去河邊找鬼扣汪。 笑死断楷,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的崭别。 我是一名探鬼主播冬筒,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼紊遵!你這毒婦竟也來了账千?” 一聲冷哼從身側(cè)響起侥蒙,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤暗膜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鞭衩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體学搜,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年论衍,在試婚紗的時候發(fā)現(xiàn)自己被綠了瑞佩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡坯台,死狀恐怖炬丸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蜒蕾,我是刑警寧澤稠炬,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站咪啡,受9級特大地震影響首启,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撤摸,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一毅桃、第九天 我趴在偏房一處隱蔽的房頂上張望褒纲。 院中可真熱鬧,春花似錦钥飞、人聲如沸莺掠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汁蝶。三九已至,卻和暖如春论悴,著一層夾襖步出監(jiān)牢的瞬間掖棉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工膀估, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幔亥,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓察纯,卻偏偏與公主長得像帕棉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子饼记,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容