FREE TRIAL: Accelerate your app development career with Hacking with Swift+! >>

SOLVED: Resizing text (change font size) of a CATextLayer

Forums > macOS

My macOS app generates PDF files using fixed size CATextLayers. Each text layer has text of different lengths and need to be word wrapped when the length of the text line exceeds the bounds of the frame. However, if the lines of text exceed the height of the bounding frame, the font size will need to be reduced by one and then rechecked to ensure the text is contained within the bounding box. I'm assuming the wrapping would be automatic if the font size is reduced, i.e. re-wrapped?

But, it seems everything I read is for UIKit and using UIFont, which doesn't work in macOS.

It appears boundingRectWithSize:options:attributes:context: might be what I need to use to get the size from my text string and then keep reducing the font size until the returned height and width are within the max size of the fixed size of my CATextLayer. Unfortunately, again, the examples I find are either for iOS or written in Objective-C.

This is what I've been toying around with, but I'm getting a crash "Bad Execution" error. Also, I do not know how to set the nsString font size or word wrapping attributes. Am I approaching this totally wrong?

import Cocoa

let myTextLayer = CATextLayer()
let rectWidth = 70
let rectHeight = 70
var fontSize = 14
var tempBox = CGSize(width: 71, height: 71)
let str = "This string has some really, really, really long text"

func sizeOfString(nsString: NSString) -> CGSize {
    return nsString.boundingRect(
    with: CGSize(width: CGFloat.infinity, height:CGFloat.infinity),
    options: NSString.DrawingOptions.usesLineFragmentOrigin,
    attributes: [NSAttributedString.Key.font: "Helvetica"],
                             context: nil).size
}

while sizeOfString(nsString: str as NSString).width > tempBox.width || sizeOfString(nsString: str as NSString).height > tempBox.height {
    fontSize -= 1
}
myTextLayer.string = str
myTextLayer.fontSize = CGFloat(fontSize)
myTextLayer.alignmentMode = .center
print(tempBox)
print(fontSize)

Appreciate any help or leading me into a direction where I can work out a solution.

   

I figured this out.

I found a way to check the boundingRect of a string based on the assigned attributes.

    func sizeOfRect(string: NSString, fontSize: CGFloat) -> Int {
        /* Credit to Jake Marsh - 12/10/2015
            https://littlebitesofcocoa.com/144-drawing-multiline-strings

            Return the height of a boundingRect for a specified string at a specified fontSize
         */
        let cellFontSize:CGFloat = fontSize
        let cellFont:NSFont = NSFont.systemFont(ofSize: cellFontSize, weight: .regular)
        let cellParagraphStyle = NSMutableParagraphStyle()
        let cellTextAttributes = [NSAttributedString.Key.font: cellFont, NSAttributedString.Key.paragraphStyle: cellParagraphStyle]
        let cellDrawingOptions: NSString.DrawingOptions = [
        .usesLineFragmentOrigin, .usesFontLeading]
        cellParagraphStyle.lineHeightMultiple = 1.0
        cellParagraphStyle.lineBreakMode = .byWordWrapping

        return Int(string.boundingRect(with: CGSize(width: 70, height: CGFloat.infinity), options: cellDrawingOptions, attributes: cellTextAttributes).height)
    }

And then I create a textLater based on this class:

class VerticallyAlignedTextLayer : CATextLayer {
        /* Credit - purebreadd - 6/24/2020
            https://stackoverflow.com/questions/4765461/vertically-align-text-in-a-catextlayer
         */

        func calculateMaxLines() -> Int {
            let maxSize = CGSize(width: frame.size.width, height: frame.size.width)
            let font = NSFont(descriptor: self.font!.fontDescriptor, size: self.fontSize)
            let charSize = floor(font!.capHeight)
            let text = (self.string ?? "") as! NSString
            let textSize = text.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font!], context: nil)
            let linesRoundedUp = Int(floor(textSize.height/charSize))
            return linesRoundedUp
        }

        override func draw(in context: CGContext) {
            let height = self.bounds.size.height
            let fontSize = self.fontSize
            let lines = CGFloat(calculateMaxLines())
            let yDiff = -(height - lines * fontSize) / 2 - lines * fontSize / 6.5 // Use -(height when in non-flipped coordinates (like macOS's default)

            context.saveGState()
            context.translateBy(x: 0, y: yDiff)
            super.draw(in: context)
            context.restoreGState()
        }
    }

I can now center text both horizontally and vertically and resize it until it fits with a specified height and width. I am only getting the height from the method, but you can get all the coordinates by returning CGRect rather than an integer.

   

Hacking with Swift is sponsored by Sentry

SPONSORED With Sentry’s error and performance monitoring for iOS you see mobile vitals that actually matter, can solve any latency issues quickly, and learn how each release is performing over time.

Get started

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.