2

I'm creating a thing that puts text on top of images. The biggest allowed frame for the text to be drawn is specified by our backend, and I have to do my best to fit whatever text the user enters into that rectangle.

I looked up a ton of older answers, but all answers seem to either use UILabel automatic fitting or just brute forcing String.boundingRect(with:options:attributes:context:).

My initial idea was to just divide use the height of the text rectangle as the point size, but this seems to not work 100% in addition to not supporting text's that will be too long horizontally.

Here's how I'm drawing stuff right now, just to get some context.

    let image = renderer.image { context in            
        backgroundImage.draw(at: .zero)

        let titleAttributes: [NSAttributedStringKey: Any] = [
            .font: UIFont(name: fontName, size: maxSize.height)!,
            .foregroundColor: UIColor.white,
            .paragraphStyle: NSMutableParagraphStyle(alignment: alignment),
            .shadow: NSShadow( color: .black, offset: .zero, radius: 28),
        ]

        (title as NSString).draw(with: maxFrame,
                                 options: .usesLineFragmentOrigin,
                                 attributes: titleAttributes,
                                 context: nil)
    }

Is there any efficient way to figure out the point size of a font to fit inside a rectangle without using UILabels or brute forcing it?

Oscar Apeland
  • 6,422
  • 7
  • 44
  • 92

1 Answers1

4

I solved it myself again!

The text I'm drawing is never allowed to break line, so it's always one line. When making a brute force method in Playgrounds, I noticed the text width is linear to font size. So that made it really easy.

extension UIFont {
    convenience init?(named fontName: String, fitting text: String, into targetSize: CGSize, with attributes: [NSAttributedStringKey: Any], options: NSStringDrawingOptions) {
        var attributes = attributes
        let fontSize = targetSize.height

        attributes[.font] = UIFont(name: fontName, size: fontSize)
        let size = text.boundingRect(with: CGSize(width: .greatestFiniteMagnitude, height: fontSize),
                                     options: options,
                                     attributes: attributes,
                                     context: nil).size

        let heightSize = targetSize.height / (size.height / fontSize)
        let widthSize = targetSize.width / (size.width / fontSize)

        self.init(name: fontName, size: min(heightSize, widthSize))
    }
}

This extension will initiate a font that's guaranteed to fit inside the target rectangle as long as it's 1 line.

Oscar Apeland
  • 6,422
  • 7
  • 44
  • 92