在Swift中,与CGPathApply相当或替代的方法是什么?

17
预发布文档中,似乎没有Swift版本的CGPathApply。是否有等效或替代方法?我正在尝试获取CGPath的所有子路径,以便可以从不同的起点重新绘制它。

你试过了吗?我看到一些其他的符号实际上在Swift中可用,但在文档中却没有显示为Swift。同时尝试cmd-click另一个CGPath函数,看看在Swift翻译模块“头文件”中会显示什么。如果它不在那里,请提交错误报告——它应该在那里。 - rickster
4个回答

37

自 2017 年底

从 iOS 11、macOS 10.13、tvOS 11 和 watchOS 4 开始,您应该使用 CGPathapplyWithBlock 方法。这里有一个真实的例子,取自这个用于圆角化 CGPath 的 Swift 包。在这段代码中,self 是一个 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

在Swift 3.0中,您可以像这样使用CGPath.apply

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

通过添加@convention(c),您现在可以直接从Swift调用CGPathApply。这是一个执行必要操作的包装器:

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)
    }
}

(请注意,我的代码中没有提到@convention(c),但在Core Graphics模块的CGPathApply声明中使用了它。)

示例用法:

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

值得注意的是,Swift中“element”的类型是UnsafePointer<CGPathElement>。 - Litherum
6
Ole Begemann 刚刚发表了一篇关于这个话题的优秀文章 http://oleb.net/blog/2015/06/c-callbacks-in-swift/。 - Gouldsc
为什么第二个参数是块?文档中写着“传递给应用函数的用户数据指针,或 NULL。”。我正在尝试将一些在该参数中传递数组的代码转换为 Swift。 - Nicolas Miari
第二个参数是一个块,因为块封装了代码和数据。您可以传递引用数组的块。 - rob mayoff

10

提示:如果你需要支持iOS 11以下的版本,请使用被接受的答案。如果可以要求iOS 11及以上版本,那么这个答案将更加简单。

自从iOS 11以来,苹果公司已经给出了官方答案:CGPath.applyWithBlock(_:)

这使得所有不必要的把戏变得不再需要,因为CGPath.apply(info:function:)是一个C函数,不能以通常的Swifty方式在函数内外传输信息。

以下代码可帮助您完成:

let pathElements = path.pathElements()

为了能够做到这一点,复制并粘贴。
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

}

或者拿这段代码作为示例,演示如何使用CGPath.applyWithBlock(_:)

为了完整起见,这是来自苹果的官方文档: https://developer.apple.com/documentation/coregraphics/cgpath/2873218-applywithblock

自从iOS 13以来,苹果提供了更加优雅的官方解决方案:使用SwiftUI(即使您的UI不在SwiftUI中)。

将您的cgPath转换为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
    }
}

element变量的类型是Path.Element,它是一个纯Swift枚举,因此甚至不需要任何技巧来从element中获取值。

为了完整起见,这是官方的Apple文档:https://developer.apple.com/documentation/swiftui/path/3059547-foreach


7
以下是Ole Begemann的优秀文章中的精华,经过Swift 3的改编,可以访问组成UIBezierPath实例的各个元素:
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
        }
    }

}

2
添加一个使用示例会使这个答案更好。 - Tommie C.
我想知道是否有可能清除一些点并使用剩余的元素点创建一个单独的UIBezierPath?基本上,我想从原始bezierPath中擦除一些路径。谢谢。 - Lasantha Basnayake

2

Dmitry Rodionov开发了一个函数,可以将Swift函数转换为CFunctionPointer(请参见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;
}

我已经成功地与CGPathApply一起使用,并使用Swift回调函数。 (代码位于http://parker-liddle.org/CGPathApply/CGPathApply.zip) 尽管如Dmitry所说,这是一个反向工程的函数而不是受支持的函数。


哇!那真是一些非常酷的黑客技巧!我认为现在有其他方法可以实现它,而不需要深入到那个程度,但是很高兴看到人们在面对严峻的编码困境时能够拥有那种能力并成功地完成这样的事情! - clearlight

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接