swiftmacosswift5cgpathnsbezierpath

How to fix convenience init w/closure Extension of NSBezierPath to use CGPath that fails in Swift 5.0?


Inline below is is a method I acquired somewhere for extending NSBezierPath using a convenience initializer that takes CGPath as an argument so that it behaves more like UIBezierPath on iOS.

It worked previously, but when I try to compile it (years later) on Swift 5, I get the the following compile time error:

A C function pointer cannot be formed by a closure that captures context

How can I resolve that?

convenience init(path : CGPath) {
    path.apply(info: nil, function: { (_, elementPointer) in
        let element = elementPointer.pointee
        switch element.type {
        case .moveToPoint:
            let points = Array(UnsafeBufferPointer(start: element.points, count: 1))
            self.move(to: points[0])
            break
        case .addLineToPoint:
            let points = Array(UnsafeBufferPointer(start: element.points, count: 1))
            self.line(to: points[0])
            break
        case .addQuadCurveToPoint:
            let points = Array(UnsafeBufferPointer(start: element.points, count: 2))
            let qp0 = self.currentPoint
            let qp1 = points[0]
            let qp2 = points[1]
            let m = CGFloat(2.0 / 3.0)
            var cp1 = NSPoint()
            var cp2 = NSPoint()
            cp1.x = (qp0.x + ((qp1.x - qp0.x) * m))
            cp1.y = (qp0.y + ((qp1.y - qp0.y) * m))
            cp2.x = (qp2.x + ((qp1.x - qp2.x) * m))
            cp2.y = (qp2.y + ((qp1.y - qp2.y) * m))
            self.curve(to: qp2, controlPoint1:cp1, controlPoint2:cp2)
        case .addCurveToPoint:
            let points = Array(UnsafeBufferPointer(start: element.points, count: 3))
            self.curve(to:points[2], controlPoint1:points[0], controlPoint2:points[1])
            break
        case .closeSubpath:
            self.close()
        @unknown default:
            break;
        }
    })
}

Solution

  • I’d suggest using path.applyWithBlock. I’d also lose all of those unswifty break statements and just access element.points directly.

    Perhaps something like:

    convenience init(path: CGPath) {
        self.init()
    
        path.applyWithBlock { elementPointer in
            let element = elementPointer.pointee
            switch element.type {
            case .moveToPoint:
                move(to: element.points[0])
    
            case .addLineToPoint:
                line(to: element.points[0])
    
            case .addQuadCurveToPoint:
                let qp0 = self.currentPoint
                let qp1 = element.points[0]
                let qp2 = element.points[1]
                let m = CGFloat(2.0 / 3.0)
                let cp1 = NSPoint(x: qp0.x + ((qp1.x - qp0.x) * m),
                                  y: qp0.y + ((qp1.y - qp0.y) * m))
                let cp2 = NSPoint(x: qp2.x + ((qp1.x - qp2.x) * m),
                                  y: qp2.y + ((qp1.y - qp2.y) * m))
                curve(to: qp2, controlPoint1: cp1, controlPoint2: cp2)
    
            case .addCurveToPoint:
                curve(to: element.points[2], controlPoint1: element.points[0], controlPoint2: element.points[1])
    
            case .closeSubpath:
                close()
    
            @unknown default:
                break
            }
        }
    }
    

    Just as a proof of concept, I created a CGPath with all of the different element types, and created a NSBezierPath from that using the above. I then stroked both of them using their respective API (the NSBezierPath in the heavy blue stroke, the CGPath in a white stroke, on top of it). It’s a quick empirical validation that the conversion logic yielded equivalent paths:

    enter image description here