swiftcgpath

Equivalent of or alternative to CGPathApply in Swift?


In the pre-release documentation there appears to be no Swift version of CGPathApply. Is there an equivalent or alternative? I'm trying to get all subpaths of a CGPath so that I can redraw it from a different starting point.


Solution

  • 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()")
        }
    }