4
\$\begingroup\$

I'm working on a draw function which needs to draw the word 'crux' in the center of the current view and then draw a red box around the 'x' character like this:

enter image description here

My code to do this looks like this

override func draw(_ rect: CGRect) {

    guard let context = UIGraphicsGetCurrentContext() else { return }

    // Flip the coordinate system
    context.textMatrix = .identity
    context.translateBy(x: 0, y: bounds.size.height)
    context.scaleBy(x: 1.0, y: -1.0)

    // Create the logo as an attributed string
    let font = UIFont(name: "EntangledPlainBRK", size:self.bounds.size.width * 0.4)!
    let attrs:[String:Any] = [
        kCTForegroundColorAttributeName as String: UIColor.black.cgColor,
        kCTFontAttributeName as String: font
    ]
    let logo = NSMutableAttributedString(string: "crux", attributes: attrs)

    // Work out the position of the logo in the view.
    let path = CGMutablePath()
    let textSize = logo.size()
    let logoOrigin = CGPoint(x: (rect.width - textSize.width) / 2.0, y: (rect.height - textSize.height) / 2.0)
    let logoRect = CGRect(origin: logoOrigin, size: logo.size())
    path.addRect(logoRect)

    // get the frame setters.
    let framesetter = CTFramesetterCreateWithAttributedString(logo as CFAttributedString)
    let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, logo.length), path, nil)

    CTFrameDraw(frame, context)

    // Set the text position of the context to the location of the run on screen.
    var lineOrigins = [CGPoint] (repeating: .zero, count: 1)
    CTFrameGetLineOrigins(frame, CFRange(), &lineOrigins)
    let lineOrigin = lineOrigins[0]
    context.textPosition = CGPoint(x:logoOrigin.x + lineOrigin.x, y:logoOrigin.y + lineOrigin.y)

    // Get the position of the last character.
    let lines = CTFrameGetLines(frame)
    let line = unsafeBitCast(CFArrayGetValueAtIndex(lines, 0), to:CTLine.self)
    let runs = CTLineGetGlyphRuns(line)
    let run = unsafeBitCast(CFArrayGetValueAtIndex(runs, 0), to:CTRun.self)
    let glyphPosition = CTRunGetImageBounds(run, context, CFRange(location:3, length:1))

    // Draw a box around it.
    context.setStrokeColor(UIColor.red.cgColor)
    context.setLineWidth(2.0)
    context.stroke(glyphPosition)

}

It took some time to work out and feels a bit messy. I was wondering if there are any Core Text experts out there who know some tricks to shorten this code.

\$\endgroup\$
2
\$\begingroup\$

You can get rid of the "unsafe bit casts", an CFArrayRef can be bridged to [CTLine] (resp. [CTRun]) directly (tested with Swift 3 and Swift 4).

If you know for sure that there is at least one line with at least one run then it would be:

let lines = CTFrameGetLines(frame) as! [CTLine]
let line = lines.first!
let runs = CTLineGetGlyphRuns(line) as! [CTRun]
let run = runs.first!

Otherwise use optional binding with if let or guard let. A most defensive version would be

guard let lines = CTFrameGetLines(frame) as? [CTLine],
    let line = lines.first,
    let runs = CTLineGetGlyphRuns(line) as? [CTRun],
    let run = runs.first else { return }
\$\endgroup\$
  • \$\begingroup\$ Cool. Those lines were bugging me too. \$\endgroup\$ – drekka Oct 11 '17 at 11:53
  • \$\begingroup\$ @drekka: Judging from the various answers at stackoverflow.com/q/24684750/1187415, it wasn't that simple in earlier Swift versions. \$\endgroup\$ – Martin R Oct 11 '17 at 11:59

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.