Equivalent of or alternative to CGPathApply in Swift?

Since Late 2017

Starting with iOS 11, macOS 10.13, tvOS 11, and watchOS 4, you should use the CGPath‘s applyWithBlock method. Here is a real example taken from this Swift package that rounds the corners of a CGPath. In this code, self is a CGPath:

self.applyWithBlock {
    let points = $0.pointee.points

    switch $0.pointee.type {
    case .moveToPoint:
        if let currentSubpath, !currentSubpath.segments.isEmpty {
            copy.append(currentSubpath, withCornerRadius: radius)
        }
        currentSubpath = .init(firstPoint: points[0])
        currentPoint = points[0]

    case .addLineToPoint:
        append(.line(start: currentPoint, end: points[0]))
        currentPoint = points[0]

    case .addQuadCurveToPoint:
        append(.quad(points[0], end: points[1]))
        currentPoint = points[1]

    case .addCurveToPoint:
        append(.cubic(points[0], points[1], end: points[2]))
        currentPoint = points[2]

    case .closeSubpath:
        if var currentSubpath {
            currentSubpath.segments.append(.line(start: currentPoint, end: currentSubpath.firstPoint))
            currentSubpath.isClosed = true
            copy.append(currentSubpath, withCornerRadius: radius)
            currentPoint = currentSubpath.firstPoint
        }
        currentSubpath = nil

    @unknown default:
        break
    }
}

Swift 3.0

In Swift 3.0, you can use CGPath.apply like this:

let path: CGPath = ...
// or let path: CGMutablePath

path.apply(info: nil) { (_, elementPointer) in
    let element = elementPointer.pointee
    let command: String
    let pointCount: Int
    switch element.type {
    case .moveToPoint: command = "moveTo"; pointCount = 1
    case .addLineToPoint: command = "lineTo"; pointCount = 1
    case .addQuadCurveToPoint: command = "quadCurveTo"; pointCount = 2
    case .addCurveToPoint: command = "curveTo"; pointCount = 3
    case .closeSubpath: command = "close"; pointCount = 0
    }
    let points = Array(UnsafeBufferPointer(start: element.points, count: pointCount))
    Swift.print("\(command) \(points)")
}

Swift 2.2

With the addition of @convention(c), you can now call CGPathApply directly from Swift. Here’s a wrapper that does the necessary magic:

extension CGPath {
    func forEach(@noescape body: @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        func callback(info: UnsafeMutablePointer<Void>, element: UnsafePointer<CGPathElement>) {
            let body = unsafeBitCast(info, Body.self)
            body(element.memory)
        }
        print(sizeofValue(body))
        let unsafeBody = unsafeBitCast(body, UnsafeMutablePointer<Void>.self)
        CGPathApply(self, unsafeBody, callback)
    }
}

(Note that @convention(c) isn’t mentioned in my code, but is used in the declaration of CGPathApply in the Core Graphics module.)

Example usage:

let path = UIBezierPath(roundedRect: CGRectMake(0, 0, 200, 100), cornerRadius: 15)
path.CGPath.forEach { element in
    switch (element.type) {
    case CGPathElementType.MoveToPoint:
        print("move(\(element.points[0]))")
    case .AddLineToPoint:
        print("line(\(element.points[0]))")
    case .AddQuadCurveToPoint:
        print("quadCurve(\(element.points[0]), \(element.points[1]))")
    case .AddCurveToPoint:
        print("curve(\(element.points[0]), \(element.points[1]), \(element.points[2]))")
    case .CloseSubpath:
        print("close()")
    }
}

Leave a Comment