【问题标题】:Trouble using callbacks with CGPattern in Swift3在 Swift3 中使用带有 CGPattern 的回调时遇到问题
【发布时间】:2017-10-27 22:43:00
【问题描述】:

我正在尝试在 Swift 中使用 CGPattern 创建彩色图案。 Apple 在Painting Colored Patterns 部分的Quartz 2D Programming Guide 中提供了一个很好的Objective-C 示例。但是从 Objective-C 转换所有这些语法并不是直截了当的。另外,我想在绘图回调中使用info 参数,并且没有这样做的示例。

这是我的第一次尝试:

class SomeShape {
    func createPattern() -> CGPattern? {
        let bounds = CGRect(x: 0, y: 0, width: someWidth, height: someHeight)
        let matrix = CGAffineTransform.identity
        var callbacks = CGPatternCallbacks(version: 0, drawPattern: nil, releaseInfo: nil)

        let res = CGPattern(info: nil, bounds: bounds, matrix: matrix, xStep: bounds.width, yStep: bounds.height, tiling: .noDistortion, isColored: true, callbacks: &callbacks)

        return res
    }
}

显然,这需要将 drawPattern 参数的正确值传递给 CGPatternCallbacks,并且我需要将 self 作为 info 参数传递给 CGPattern 初始化程序。

完成这个的正确语法是什么?

【问题讨论】:

  • 分享另一种方法 - 不建议作为答案,因为它没有解决如何在 Swift 中使用 CGPattern,但我决定绕过 CGPattern 并只使用 NSImage 工具进行类似画笔的绘图和区域填充,即:“在没有 CGPattern 的 Swift 中类似画笔的绘图”blog.mathaesthetics.com/2019/11/…

标签: swift callback core-graphics


【解决方案1】:

让我们从CGPatternDrawPatternCallback 开始。定义为:

typealias CGPatternDrawPatternCallback = (UnsafeMutableRawPointer?, CGContext) -> Void

所以它是一个带有两个参数的闭包 - info 和绘图上下文。

使用该信息,您可以创建CGPatternCallback,如下所示:

var callbacks = CGPatternCallbacks(version: 0, drawPattern: { (info, ctx) in
    // Drawing code here
}, releaseInfo: { (info) in {
    // Cleanup code here
})

但是这里有一些重要的事情需要注意。这些闭包的主体不能捕获块之外的任何东西。如果您尝试这样做,您将收到以下错误:

不能从捕获上下文的闭包中形成 C 函数点

这就是为什么需要使用info 参数的原因。您可以在创建模式时将self 或其他一些对象作为info 参数传递并在绘图回调中使用它。但这不是一项简单的任务,因为您不能简单地将self 作为info 参数传递。您需要将其转换为所需的UnsafeMutableRawPointer,然后将其从绘图回调中的指针转换回来。

以下是所有设置的完整代码:

class SomeShape {
    func createPattern() -> CGPattern? {
        let bounds = CGRect(x: 0, y: 0, width: someWidth, height: someHeight) // The size of each tile in the pattern
        let matrix = CGAffineTransform.identity // adjust as needed
        var callbacks = CGPatternCallbacks(version: 0, drawPattern: { (info, ctx) in
            let shape = unsafeBitCast(info, to: SomeShape.self)

            // The needed drawing code to draw one tile of the pattern into "ctx"
        }, releaseInfo: { (info) in 
            // Any cleanup if needed
        })

        let unsafeSelf = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)

        let res = CGPattern(info: unsafeSelf, bounds: bounds, matrix: matrix, xStep: bounds.width, yStep: bounds.height, tiling: .noDistortion, isColored: true, callbacks: &callbacks)

        return res
    }
}

要使用CGPattern,您可以执行以下操作:

func draw(_ ctx: CGContext) {
    // Any other needed setup

    let path = CGPath(....) // some path

    // Code to fill a path with the pattern
    ctx.saveGState()
    ctx.addPath(path) // The path to fill

    // Setup the pattern color space for the colored pattern
    if let cs = CGColorSpace(patternBaseSpace: nil) {
        ctx.setFillColorSpace(cs)
    }

    // Create and apply the pattern and its opacity
    if let fillPattern = someShapeInstance.createPattern() {
        var fillOpacity = CGFloat(1.0)
        ctx.setFillPattern(fillPattern, colorComponents: &strokeOpacity)
    }

    ctx.fillPath(using: theDesiredFillRule)
    ctx.restoreGState()

    // Any other drawing
}

当使用彩色图案(相对于模板、非彩色图案)时,您必须在设置填充图案之前设置填充颜色空间。

您还可以使用图案来描边路径。只需使用setStrokeColorSpacesetStrokePattern

【讨论】:

  • maddy,你有什么想法,为什么 Apple 将 CGPatternDrawPatternCallback 的第一个参数声明为可变版本的指针?
  • @user3441734 在 Objective-C 中 info 是一个普通的旧 void *。我真的没有以前在 Swift 中使用 Unsafe[Mutable][Raw]Pointer 的经验,所以我不能肯定地说。但我的猜测是,通过将其设为UnsafeMutableRawPointer 而不是UnsafeRawPointer,它可以让您在使用info 参数的用途以及如何使用它方面有更多选择。如果还没有关于此主题的单独问题,我建议发布一个单独的问题。
  • 值得注意的是,这里有两个主要的未定义行为机会。首先,如果self 在绘制模式之前被释放,您将在尝试在绘图回调中使用悬空指针时得到未定义的行为。其次(不太重要,因为您可以控制它)如果您将nil 传递给info: 参数,您将在尝试使用nil 作为非可选参考时获得未定义的行为。使用Unmanaged,例如in my answer,在引用和不透明指针之间进行转换可以防止这两种情况。
  • (如果将非指针大小的实例传递到 unsafeBitCast,则为 UB 提供第三次机会,例如,如果 SomeShape 意外更改为 struct 可能会出现这种情况在重构中——Unmanaged 静态地强制你转换的东西是一个引用)。话虽这么说,但其中的第 2 点和第 3 点实际上只是吹毛求疵 :)
  • @Hamish 1) 在我的情况下,保证self 在绘制模式之前不能被释放。 2) 我没有理由将nil 传递给info 参数,然后尝试在drawPattern 回调中使用它。 3)这都是类层次结构的一部分,因此没有机会将其重构为结构。不过,这些都是需要指出的好东西。谢谢。
【解决方案2】:

正如你所说的in your answerCGPatternDrawPatternCallback 定义为:

typealias CGPatternDrawPatternCallback =
                               @convention(c) (UnsafeMutableRawPointer?, CGContext) -> Void

@convention(c) 属性(仅出现在生成的标头中)意味着使用的函数值必须与 C 兼容,因此不能捕获任何上下文(因为 C 函数值只不过是指向一个函数,并且不存储额外的上下文对象)。

所以如果你想在函数中提供上下文,你需要将你自己的UnsafeMutableRawPointer? 传递给CGPattern's initialiserinfo: 参数。然后,这将在被调用时作为给定绘图模式函数的第一个参数传递。

为了将self传递给这个参数,你可以使用Unmanaged。这允许您在引用和不透明指针之间进行转换,并且与 unsafeBitCast 不同,还允许您在执行此操作时控制引用的内存管理。

鉴于我们不能保证createPattern() 的调用者会保留self,我们不能只是将它传递给info: 参数而不自己保留它。如果它在没有保留的情况下传递(例如使用unsafeBitCast),然后在绘制模式之前被释放 - 当您尝试在绘图回调中使用悬空指针时,您会得到未定义的行为。 p>

Unmanaged:

  • 您可以使用 passRetained(_:).toOpaque() 将引用作为 +1 保留不透明指针传递

  • 您可以使用 fromOpaque(_:).takeUnretainedValue() 从此指针取回引用(并且实例将保持保留)

  • 然后您可以使用 fromOpaque(_:).release() 使用 +1 保留。当CGPattern 被释放时,你会想要这样做。

例如:

class SomeShape {
    // the bounds of the shape to draw
    let bounds = CGRect(x: 0, y: 0, width: 40, height: 40)

    func createPattern() -> CGPattern? {

        var callbacks = CGPatternCallbacks(version: 0, drawPattern: { info, ctx in

            // cast the opaque pointer back to a SomeShape reference.
            let shape = Unmanaged<SomeShape>.fromOpaque(info!).takeUnretainedValue()

            // The code to draw a single tile of the pattern into "ctx"...
            // (in this case, two vertical strips)
            ctx.saveGState()
            ctx.setFillColor(UIColor.red.cgColor)
            ctx.fill(CGRect(x: 0, y: 0,
                            width: shape.bounds.width / 2, height: shape.bounds.height))

            ctx.setFillColor(UIColor.blue.cgColor)
            ctx.fill(CGRect(x: 20, y: 0,
                            width: shape.bounds.width / 2, height: shape.bounds.height))
            ctx.restoreGState()

        }, releaseInfo: { info in
            // when the CGPattern is freed, release the info reference,
            // consuming the +1 retain when we originally passed it to the CGPattern.
            Unmanaged<SomeShape>.fromOpaque(info!).release()
        })

        // retain self before passing it off to the info: parameter as an opaque pointer.
        let unsafeSelf = Unmanaged.passRetained(self).toOpaque()

        return CGPattern(info: unsafeSelf, bounds: bounds, matrix: .identity,
                         xStep: bounds.width, yStep: bounds.height,
                         tiling: .noDistortion, isColored: true, callbacks: &callbacks)
    }
}

或者,如果您想要 SomeShape 的值语义,一个更好的解决方案,您可以将其设为 struct。然后在创建模式时,您可以将其包装在 Context heap-allocated 框中,然后将其传递给 info: 参数:

struct SomeShape {

    // the bounds of the shape to draw
    let bounds = CGRect(x: 0, y: 0, width: 40, height: 40)

    func createPattern() -> CGPattern? {

        final class Context {
            let shape: SomeShape
            init(_ shape: SomeShape) { self.shape = shape }
        }

        var callbacks = CGPatternCallbacks(version: 0, drawPattern: { info, ctx in

            // cast the opaque pointer back to a Context reference,
            // and get the wrapped shape instance.
            let shape = Unmanaged<Context>.fromOpaque(info!).takeUnretainedValue().shape

            // ...

        }, releaseInfo: { info in
            // when the CGPattern is freed, release the info reference,
            // consuming the +1 retain when we originally passed it to the CGPattern.
            Unmanaged<Context>.fromOpaque(info!).release()
        })

        // wrap self in our Context box before passing it off to the info: parameter as a
        // +1 retained opaque pointer.
        let unsafeSelf = Unmanaged.passRetained(Context(self)).toOpaque()

        return CGPattern(info: unsafeSelf, bounds: bounds, matrix: .identity,
                         xStep: bounds.width, yStep: bounds.height,
                         tiling: .noDistortion, isColored: true, callbacks: &callbacks)
    }
}

现在这也解决了任何保留周期问题。

【讨论】:

  • 感谢您的信息。这是我第一次在 Swift 中处理“指针”。我在四处搜索时遇到了unsafeBitCast 的用法。它适用于我的情况,但很高兴知道有更好的方法。在我的真实代码中,我实际上将CALayer 的一个实例作为info 参数传递。这是在createPattern 方法中创建的局部变量。它在我的代码中使用unsafeBitCast 工作得很好。包装CALayer 类型的局部变量更安全的方法是什么?
  • @rmaddy 我假设您将CALayer 实例局部变量添加到层层次结构中,然后将其保留(或其他一些东西保留它)?这是我能想到的唯一方法,为什么它会与unsafeBitCast 一起工作。除非存在保留周期问题(即CALayer 实例保留对CGPattern 的引用),我仍然建议保留CALayer 实例和Unmanaged(与第一个示例完全相同),然后再将其传递给info: 参数——只是要防止它在绘制模式之前被释放。
  • 如果存在保留周期问题,您始终可以创建一个 Weak 类包装器,其中包含对您给定层的弱引用,然后传递保留的包装器。但是当然,如​​果给定的CALayer 始终是给定CGPattern 的唯一所有者,那么你不应该做任何额外的保留(实际上是unowned)。
  • 虽然即使你没有做额外的保留,我仍然建议在这种情况下使用Unmanaged API 而不是unsafeBitCast,因为它具有更强的静态保证(例如确保你'重新使用引用并让您打开可选选项)——在没有任何保留/释放的情况下进行转换只需 passUnretained(_:).toOpaque()fromOpaque(_:).takeUnretainedValue()Martin's convenience functions here 可以派上用场:)
  • 其实本地变量引用的CALayer没有其他引用。它没有添加到任何层层次结构中,据我所知,层上没有保留周期。但为了安全起见,我转而使用Unmanaged,它仍在工作。谢谢。
猜你喜欢
  • 2012-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-21
  • 1970-01-01
  • 1970-01-01
  • 2020-09-07
相关资源
最近更新 更多