1

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;
        }
    })
}
clearlight
  • 12,255
  • 11
  • 57
  • 75
  • I don't know under which you are writing this code. Assuming that it's a subclass of `NSView`, I have never seen a single where somebody uses a closure in it. – El Tomato Dec 28 '19 at 00:33
  • 1
    @Rob, I wondered. I had started to do that, but I'll fix it. Thanks. – clearlight Dec 28 '19 at 01:24

2 Answers2

1

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

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks @Rob! I will look this up and try to dissect it more. Can you explain to me what the error was really getting at, and how applyWithBlock is different and why it fixed it? I wonder why the original solution did go with the array based on an unsafe pointer. Do you think it had anything to do with it being from an earlier Swift version? – clearlight Dec 28 '19 at 00:52
  • Looks like `CGPath.applyWithBlock()` is more Swiftified? Looks like the `.apply()` method pushes the native C mechanisms further and unnecessarily complicates it. So I think I understand what you did essentially. But not quite sure what the compiler was complaining about or why the original author used the Array() class. – clearlight Dec 28 '19 at 01:01
  • Thanks again! Excellent. I have done a fair amount of Swift but day job (Linux/C/virtualization) has kept me away and I've forgotten things. Review of UnsafePointer helped. So I think the answer is pretty clear to me now. Saved me a lot of time and helps get my Swift hat on a little swifter. Really appreciate it. – clearlight Dec 28 '19 at 01:08
  • The `apply` dates back to OS 10.2, whereas `applyWithBlock` was introduced in 10.13, undoubtedly to avoid the limitations of the older syntax. And obviously the bridging of blocks to C functions has evolved over time. Re the use of `Array`, that may have just been the author’s unfamiliarity with working with pointers. Judging from the presence of the `break` statements, which have never been required, it suggests someone new to Swift: I’m not sure I’d invest too much time trying to divine the original author’s rationale. – Rob Dec 28 '19 at 01:13
  • Thanks. Nice arcana that pretty much closes the loop. Pristine educational experience all the way around. – clearlight Dec 28 '19 at 01:29
1

This is a supplemental 'answer' to the question... it is the complimentary function to the convenience method in the question that @Rob fixed, which converts the NSBezierPath to a CGPath. The two together make it convenient to port between macOS and iOS for example, and use NSBezierPath more easily along side other CoreGraphics code.

private func transformToCGPath() -> CGPath {

    let path = CGMutablePath()
    let points = UnsafeMutablePointer<NSPoint>.allocate(capacity: 3)
    let numElements = self.elementCount

    if numElements > 0 {

        var didClosePath = true

        for index in 0..<numElements {

            let pathType = self.element(at: index, associatedPoints: points)

            switch pathType {
            case .moveTo:
                path.move(to: CGPoint(x: points[0].x, y: points[0].y))
            case .lineTo:
                path.addLine(to: CGPoint(x: points[0].x, y: points[0].y))
                didClosePath = false
            case .curveTo:
                path.addCurve(to: CGPoint(x: points[0].x, y: points[0].y), control1: CGPoint(x: points[1].x, y: points[1].y), control2: CGPoint(x: points[2].x, y: points[2].y))
                didClosePath = false
            case .closePath:
                path.closeSubpath()
                didClosePath = true
            @unknown default:
                print("Warning! New NSBezierPath.ElementTypes() added, may affect transformToCGPath!")
            }
        }

        if !didClosePath { path.closeSubpath() }
    }
    points.deallocate()
    return path
}
clearlight
  • 12,255
  • 11
  • 57
  • 75