Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Equivalent of or alternative to CGPathApply in Swift?

Tags:

swift

cgpath

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.

like image 910
JuJoDi Avatar asked Jun 17 '14 23:06

JuJoDi


4 Answers

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()")
    }
}
like image 58
rob mayoff Avatar answered Nov 19 '22 15:11

rob mayoff


(Hint: if you have to support an iOS before iOS 11, use the accepted answer. If you can require iOS 11, this answer is much easier.)

Since iOS 11, there is an official answer from Apple to this question: CGPath.applyWithBlock(_:).

This makes all the dirty tricks unnecessary that come from the problem that CGPath.apply(info:function:) is a C function that does not allow information transported in and out of the function in a usual swifty way.

The following code allows you to do:

let pathElements = path.pathElements()

To be able to do that, copy & paste

import CoreGraphics

extension CGPath {
    func pathElements() -> [PathElement] {
        
        var result = [PathElement]()

        self.applyWithBlock { (elementPointer) in
            let element = elementPointer.pointee
            switch element.type {
            case .moveToPoint:
                let points = Array(UnsafeBufferPointer(start: element.points, count: 1))
                let el = PathElement.moveToPoint(points[0])
                result.append(el)
            case .addLineToPoint:
                let points = Array(UnsafeBufferPointer(start: element.points, count: 1))
                let el = PathElement.addLineToPoint(points[0])
                result.append(el)
            case .addQuadCurveToPoint:
                let points = Array(UnsafeBufferPointer(start: element.points, count: 2))
                let el = PathElement.addQuadCurveToPoint(points[0], points[1])
                result.append(el)
            case .addCurveToPoint:
                let points = Array(UnsafeBufferPointer(start: element.points, count: 3))
                let el = PathElement.addCurveToPoint(points[0], points[1], points[2])
                result.append(el)
            case .closeSubpath:
                result.append(.closeSubpath)
            @unknown default:
                fatalError()
            }
        }
        
        return result
    }

}

public enum PathElement {

    case moveToPoint(CGPoint)
    case addLineToPoint(CGPoint)
    case addQuadCurveToPoint(CGPoint, CGPoint)
    case addCurveToPoint(CGPoint, CGPoint, CGPoint)
    case closeSubpath

}

or take this code as an example to how to use CGPath.applyWithBlock(_:) yourself.

For completeness, this is the official documentation from Apple: https://developer.apple.com/documentation/coregraphics/cgpath/2873218-applywithblock

Since iOS 13 there is an even more elegant official answer from Apple: Use SwiftUI (even if your UI isn't in SwiftUI)

Transform your cgPath into a SwiftUI Path

let cgPath = CGPath(ellipseIn: rect, transform: nil)
let path =  Path(cgPath)
path.forEach { element in
    switch element {
    case .move(let to):
        break
    case .line(let to):
        break
    case .quadCurve(let to, let control):
        break
    case .curve(let to, let control1, let control2):
        break
    case .closeSubpath:
        break
    }
}

Variable element is of type Path.Element which is a pure Swift Enum, so there aren't even tricks necessary to get the values out of element.

For completeness, this is th official Apple documentation: https://developer.apple.com/documentation/swiftui/path/3059547-foreach

like image 33
Gerd Castan Avatar answered Nov 19 '22 13:11

Gerd Castan


Here's the highlights from Ole Begemann's great post (thanks @Gouldsc!), adapted for Swift 3, which allows for accessing the individual elements composing a UIBezierPath instance:

extension UIBezierPath {

    var elements: [PathElement] {
        var pathElements = [PathElement]()
        withUnsafeMutablePointer(to: &pathElements) { elementsPointer in
            cgPath.apply(info: elementsPointer) { (userInfo, nextElementPointer) in
                let nextElement = PathElement(element: nextElementPointer.pointee)
                let elementsPointer = userInfo!.assumingMemoryBound(to: [PathElement].self)
                elementsPointer.pointee.append(nextElement)
            }
        }
        return pathElements
    }

}

public enum PathElement {

    case moveToPoint(CGPoint)
    case addLineToPoint(CGPoint)
    case addQuadCurveToPoint(CGPoint, CGPoint)
    case addCurveToPoint(CGPoint, CGPoint, CGPoint)
    case closeSubpath

    init(element: CGPathElement) {
        switch element.type {
        case .moveToPoint: self = .moveToPoint(element.points[0])
        case .addLineToPoint: self = .addLineToPoint(element.points[0])
        case .addQuadCurveToPoint: self = .addQuadCurveToPoint(element.points[0], element.points[1])
        case .addCurveToPoint: self = .addCurveToPoint(element.points[0], element.points[1], element.points[2])
        case .closeSubpath: self = .closeSubpath
        }
    }

}
like image 44
khaullen Avatar answered Nov 19 '22 15:11

khaullen


Dmitry Rodionov has produced a function for converting a Swift function to a CFunctionPointer (see https://github.com/rodionovd/SWRoute/wiki/Function-hooking-in-Swift).

#define kObjectFieldOffset sizeof(uintptr_t)

struct swift_func_object {
    uintptr_t *original_type_ptr;
#if defined(__x86_64__)
    uintptr_t *unknown0;
#else
    uintptr_t *unknown0, *unknown1;
#endif
    uintptr_t function_address;
    uintptr_t *self;
};


uintptr_t _rd_get_func_impl(void *func)
{
    struct swift_func_object *obj = (struct swift_func_object *)*(uintptr_t *)(func + kObjectFieldOffset);
    
    //printf("-->Address of C-Func %lx unk=%lx ori=%lx<--\n", obj->function_address, obj->unknown0, obj->original_type_ptr);
    return obj->function_address;
}

I am using this successfully with CGPathApply along with a Swift callback function. (code at http://parker-liddle.org/CGPathApply/CGPathApply.zip) Although as Dmitry says this is a reverse engineered function and not a supported one.

like image 2
Gary Liddle Avatar answered Nov 19 '22 14:11

Gary Liddle