作者: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)寫過一篇關于如何獲取 CGPath
和 UIBezierPath
中元素的文章栈戳∑癜粒可以通過調(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)用 map
或 filter
方法。
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()
也可以迭代 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 當中進行這樣的操作會有點繞,不過卻是完全可能的袒哥。