Search code examples
iosswiftuikitcore-graphicsuigraphicscontext

Calculate point size to fit string inside a rectangle


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?


Solution

  • 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.