26

How can I get an array with all the CGPoint(s) contained in a given CGPath (CGMutablePathRef)?

atxe
  • 5,029
  • 2
  • 36
  • 50
jeddi
  • 651
  • 1
  • 12
  • 21

4 Answers4

64

Using Swift 2.x (for Swift 3.x, Swift 4.x and Swift 5.x read here below..) , i've found this fantastic article about C Callbacks in Swift.

Trying to obtain "all the CGPoint(s)", as explained by Lily Ballard, can be a bad idea as she said.

So, I think maybe the best way is to get the path elements points used to create a particular CGPath:

//MARK: - CGPath extensions
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)
    }

    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.MoveToPoint:
                arrayPoints.append(element.points[0])
            case .AddLineToPoint:
                arrayPoints.append(element.points[0])
            case .AddQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .AddCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }
}

With this extension you can do for example:

var bezier = UIBezierPath(ovalInRect: CGRectMake(0, 0, 400, 300))
let myOval = bezier.CGPath
let junctionPoints = myOval.getPathElementsPoints()
print("junction points are: \(junctionPoints)")

Swift 3.x and Swift 4.1 (look below for Swift 4.2 or major..)

(there are some corrections due to syntax re-introduction of @convention(c)):

extension CGPath {

    func forEach( body: @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        let callback: @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CGPathElement>) -> Void = { (info, element) in
            let body = unsafeBitCast(info, to: Body.self)
            body(element.pointee)
        }
        print(MemoryLayout.size(ofValue: body))
        let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
        self.apply(info: unsafeBody, function: unsafeBitCast(callback, to: CGPathApplierFunction.self))
    }


    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }

    func getPathElementsPointsAndTypes() -> ([CGPoint],[CGPathElementType]) {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        var arrayTypes : [CGPathElementType]! = [CGPathElementType]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            default: break
            }
        }
        return (arrayPoints,arrayTypes)
    }
}

Swift > 4.1 (also Swift 5.x) and iOS 9.x and > compatible

extension CGPath {
    func forEach( body: @escaping @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        let callback: @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CGPathElement>) -> Void = { (info, element) in
            let body = unsafeBitCast(info, to: Body.self)
            body(element.pointee)
        }
        //print(MemoryLayout.size(ofValue: body))
        let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
        self.apply(info: unsafeBody, function: unsafeBitCast(callback, to: CGPathApplierFunction.self))
    }
    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }
    func getPathElementsPointsAndTypes() -> ([CGPoint],[CGPathElementType]) {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        var arrayTypes : [CGPathElementType]! = [CGPathElementType]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            default: break
            }
        }
        return (arrayPoints,arrayTypes)
    }
}
Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • Thank you! This works in Xcode 8.3.2. (Though I had to rename forEach() to prevent conflicts with other CGPath/UIBezierPath extensions.) On that topic, I now prefix all extensions, to give them a poor-man's namespace. – Womble May 30 '17 at 21:53
  • @Alessandro Ornano: After I use linePath.apply(CGAffineTransform(rotationAngle: the getPathElementsPoints() returns me an empty array for some reason. Do you have an idea why this is happening and how to fix? – k4dt32o Dec 30 '17 at 10:55
  • I get the error message `Converting non-escaping value to 'T' may allow it to escape` on `let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)` when using Xcode10 beta6, Swift 4.2 – smnk Sep 05 '18 at 15:18
  • @smnk Thank you for your report, I've update my answer – Alessandro Ornano Sep 06 '18 at 07:53
  • Thank you so much, again. This is extremely helpful. – Womble Sep 24 '18 at 23:17
  • @Alessandro Ornano Why do you need `unsafeBitCast(callback, to: CGPathApplierFunction.self)` if callback can be modified to use optional `UnsafeMutableRawPointer?`? – Alexander Volkov Nov 05 '21 at 09:06
20

Apple added instance method CGPath.applyWithBlock, available for iOS11.0+ and macOS10.13+

You still can examine each element of a path with CGPath.apply as explained in previous answers. But if you want to avoid using C style pointers and unsafeBitCast, you should use the much more convenient instance method applyWithBlock. For example, if you want to get an array of CGPoints of a CGPath you can add an extension to CGPath, in this example a computed property(points) that collects the CGPoints of the CGPath:

/// Extension to collect CGPath points
extension CGPath {

  /// this is a computed property, it will hold the points we want to extract
  var points: [CGPoint] {

     /// this is a local transient container where we will store our CGPoints
     var arrPoints: [CGPoint] = []

     // applyWithBlock lets us examine each element of the CGPath, and decide what to do
     self.applyWithBlock { element in

        switch element.pointee.type
        {
        case .moveToPoint, .addLineToPoint:
          arrPoints.append(element.pointee.points.pointee)

        case .addQuadCurveToPoint:
          arrPoints.append(element.pointee.points.pointee)
          arrPoints.append(element.pointee.points.advanced(by: 1).pointee)

        case .addCurveToPoint:
          arrPoints.append(element.pointee.points.pointee)
          arrPoints.append(element.pointee.points.advanced(by: 1).pointee)
          arrPoints.append(element.pointee.points.advanced(by: 2).pointee)

        default:
          break
        }
     }

    // We are now done collecting our CGPoints and so we can return the result
    return arrPoints

  }
}
joqqy
  • 408
  • 4
  • 11
  • This works really well. I don't use the .addQuadCurveToPoint and .addCurveToPoint though. This answer should be uprated!! – ItsMeDom Nov 24 '18 at 13:06
15

You can use CGPathApply() to iterate over every segment in the path and run a custom function with that segment. That will give you all the information the path has.

However, if by "all the CGPoint(s)", you meant every point that has a pixel rendered to it, that's an infinitely-sized set. Although you could certainly use the apply function to get each segment, and then for every non-move segment, evaluate your own math with the segment's control points to get a list of points at whatever density you want.

Kane Cheshire
  • 1,654
  • 17
  • 20
Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
6

A CGPath is an opaque data type and does not necessarily store all the points used. In addition to this, a path may not actually draw all the points used as input (for example, consider Bézier control points).

The only two documented ways of getting information out of a path is to use CGPathGetBoundingBox to get the bounding box, or the more complicated method of using CGPathApply to call a callback function that will give you a sequence if CGPathElement types.

Krumelur
  • 31,081
  • 7
  • 77
  • 119
  • The aim is to draw a stored CGpath as a Bezier path. It's a drawing program. Basically the path that is drawn freehand on screen is stored in an array for undo/redo purposes. I am looking for a way of retrieving the stored path from the array and draw it as a Bezier rather than a standard path. That's why I was trying to obtain all the points in the original path and then process them through the Bezier routine. Is this the wrong approach? Do you have any better solution for this? I don't want to use NSundomanager and I cannot store the paths as images as this is too CPU intensive. Cheers – jeddi Oct 21 '12 at 06:10
  • 1
    Why not just store the points in an array and generate the paths when you draw them? – Krumelur Oct 21 '12 at 08:32
  • I thought about this too but then I thought it would be easier to store the paths in an array for undo purposes (I will be dealing with different paths). If I do store each path's points as you suggest I would then need to create an array of CGPoints arrays. I am not too sure how to do this! Any idea pls? – jeddi Oct 21 '12 at 09:30
  • Basically, a `CGPath` is an array of points, only a more complex and not as transparent to the application programmer. If you want to work with the points, it is probably easier to manage the data structure yourself. – Krumelur Oct 21 '12 at 10:48