9

I am trying to draw a string in the overriden draw method of CALayer (I'm programming for iOS).

override func draw(in ctx: CGContext) {
    let font = UIFont.systemFont(ofSize: 30)
    let string = NSAttributedString(string: "23", attributes: [NSAttributedStringKey.font: font])
    string.draw(at: CGPoint(x: 200, y: 200))
}

This is however not drawing anything (at least nothing is visible). Changing fill and stroke color does not make a difference.

If I draw a line it will show, so the function is being called. I know there is a CATextLayer but I need to draw the string directly. How are you supposed to draw a string in CGContext in the Swift 4 era? No amount of net searching has yielded an answer.

Melodius
  • 2,505
  • 3
  • 22
  • 37
  • I can't test it right now, but I know that not calling `super` on `draw(ctx:)` will probably be [problematic](https://stackoverflow.com/a/38689251/8558606) – Guilherme Matuella Oct 18 '18 at 12:13
  • CALayer documentation says: "The default implementation of this method does not do any drawing itself." Just to make sure I tried the code with a super-call: no difference. – Melodius Oct 18 '18 at 13:12
  • I'm sorry, I don't know why I've associated your question with the UIView drawRect, my bad. – Guilherme Matuella Oct 18 '18 at 14:32

2 Answers2

19

I assume you know all other settings. The key here is you have not make the CGContext as current one. Just add two lines code to solve the problem. Hope you get the answer.

   override func draw(in ctx: CGContext) {
   UIGraphicsPushContext(ctx)
    let font = UIFont.systemFont(ofSize: 30)
    let string = NSAttributedString(string: "23", attributes: [NSAttributedString.Key.font: font])
    string.draw(at: CGPoint(x: 200, y: 200))
   UIGraphicsPopContext()
}
E.Coms
  • 11,065
  • 2
  • 23
  • 35
  • 2
    This solved it. Thanx a lot! I find it confusing that `CGContext` has all kinds of drawing methods but none to do basic text drawing. – Melodius Oct 18 '18 at 14:41
6

The above answer works if a view is focused.

Unfortunately, it won't work in an offscreen Bitmap or CALayer context.

The right and universal way to draw a string in any CGContext is to use the CoreText api. It will work on all Apple platforms, and has a lot of power under the hood.

https://developer.apple.com/documentation/coretext

Example:

import Foundation
import Quartz

/// LazyFoxLayer
///
/// Will draw the "The lazy fox…" string in the bottom right corner of the layer,
/// with a margin of 10 pixels.

class LazyFoxLayer: CALayer {

    func draw(ctx: CGContext) {
        ctx.saveGState()
    
        // Parameters

        let margin: CGFloat = 10
        let color = CGColor.black
        let fontSize: CGFloat = 32
        // You can use the Font Book app to find the name
        let fontName = "Chalkboard" as CFString 
        let font = CTFontCreateWithName(fontName, fontSize, nil)

        let attributes: [NSAttributedString.Key : Any] = [.font: font, .foregroundColor: color]

        // Text

        let string = "The lazy fox…"
        let attributedString = NSAttributedString(string: string, 
                                                  attributes: attributes)

        // Render

        let line = CTLineCreateWithAttributedString(attributedString)
        let stringRect = CTLineGetImageBounds(line, ctx)

        ctx.textPosition = CGPoint(x: bounds.maxX - stringRect.width - margin, 
                                   y: bounds.minY + margin)

        CTLineDraw(line, ctx)

        ctx.restoreGState()
    }
}

Cheers :)

Seshadri
  • 3
  • 3
Moose
  • 2,607
  • 24
  • 23
  • This worked very well. Thank you! One issue we're running into is the text is a little blurry. Is there anything we can to do to improve the drawing resolution or hit a pixel boundary better? – Evan Moran Apr 21 '21 at 14:32
  • Not sure..Maybe use standard text sizes (9, 11, 13) and maybe align text position on normalised values ( round(x), round(y) ). Check that CGContext.allowsAntialiasing is set to true. – Moose Apr 21 '21 at 14:52
  • 1
    It turned out “layer.contentsScale = 2” fixed the blurry rendering. Much appreciated again Moose. This was the only approach that we could find that worked directly on layers! – Evan Moran Apr 22 '21 at 15:07
  • 2
    FWIW, in more recent Swift versions, you'll need something like this: `let attributes: [NSAttributedString.Key : Any]` – Ryan Aug 06 '21 at 22:53