Search code examples
swiftstringmacoscalayercgcontext

How do you draw a string on macOS in overriden draw-function of a custom CALayer subclass?


Why is the following code not drawing a string in a macOS application?

class MyLayer: CALayer {
    override func draw(in ctx: CGContext) {
        let font = NSFont.systemFont(ofSize: 18)
        let text = "TEXT"
        let textRect = CGRect(x: 100, y: 100, width: 100, height: 100)
        text.draw(in: textRect, withAttributes: [.font: font])
    }
}

Solution

  • CALayer‘s draw(in:) method is built on top of Core Graphics. All Core Graphics drawing functions take a CGContext as an argument (or, in Swift, are methods on CGContext). That’s why Core Animation passes a CGContext to your draw(in:) method.

    However, the draw(in:withAttributes:) method on String is not part of Core Graphics. It is part of AppKit. AppKit’s drawing methods don’t operate directly on a CGContext. They operate on an NSGraphicsContext (which wraps a CGContext). But, as you can see from the draw(in:withAttributes:) method, AppKit’s drawing functions don’t take an NSGraphicsContext argument and aren’t methods on NSGraphicsContext.

    Instead, there’s a global (per-thread) NSGraphicsContext. The AppKit drawing methods use this global context. Since you’re writing code down at the Core Animation level, AppKit didn’t set up a global NSGraphicsContext for you. You need to set it up yourself:

    class MyLayer: CALayer {
        override func draw(in ctx: CGContext) {
            let nsgc = NSGraphicsContext(cgContext: ctx, flipped: false)
            NSGraphicsContext.current = nsgc
    
            let font = NSFont.systemFont(ofSize: 18)
            let text = "TEXT"
            let textRect = CGRect(x: 100, y: 100, width: 100, height: 100)
            text.draw(in: textRect, withAttributes: [.font: font])
        }
    }