Calculate Font Size to Fit Frame - Core Text - NSAttributedString - iOS

Viewed 25146

I have some text which I am drawing into a fixed frame via an NSAttributedString (code below). At the moment I am hard coding the text size to 16. My question is, is there a way to calculate the best fit size for the text for the given frame ?

- (void)drawText:(CGContextRef)contextP startX:(float)x startY:(float)
y withText:(NSString *)standString
{
    CGContextTranslateCTM(contextP, 0, (bottom-top)*2);
    CGContextScaleCTM(contextP, 1.0, -1.0);

    CGRect frameText = CGRectMake(1, 0, (right-left)*2, (bottom-top)*2);

    NSMutableAttributedString * attrString = [[NSMutableAttributedString alloc] initWithString:standString];
    [attrString addAttribute:NSFontAttributeName
                      value:[UIFont fontWithName:@"Helvetica-Bold" size:16.0]
                      range:NSMakeRange(0, attrString.length)];

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)(attrString));
    struct CGPath * p = CGPathCreateMutable();
    CGPathAddRect(p, NULL, frameText);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0,0), p, NULL);

    CTFrameDraw(frame, contextP);
}
13 Answers

A little trick helps to make use of sizeWithAttributes: without the need of iterating for the right result:

NSSize sampleSize = [wordString sizeWithAttributes:
    @{ NSFontAttributeName: [NSFont fontWithName:fontName size:fontSize] }];
CGFloat ratio = rect.size.width / sampleSize.width;
fontSize *= ratio;

Make sure the fontSize for the sample is big enough to get good results.

Apple doesn't provides any method to find out a font size which fits the text in a given rect. Idea is to find out an optimal font size which perfectly fits the given size based on BinarySearch. Following extension tries different font sizes to converge to a perfect font size value.

import UIKit

extension UITextView {
    @discardableResult func adjustFontToFit(_ rect: CGSize, minFontSize: CGFloat = 5, maxFontSize: CGFloat = 100, accuracy: CGFloat = 0.1) -> CGFloat {
        // To avoid text overflow
        let targetSize = CGSize(width: floor(rect.width), height: rect.height)

        var minFontSize = minFontSize
        var maxFontSize = maxFontSize
        var fittingSize = targetSize

        while maxFontSize - minFontSize > accuracy {
            let midFontSize = (minFontSize + maxFontSize) / 2
            font = font?.withSize(midFontSize)
            fittingSize = sizeThatFits(targetSize)

            if fittingSize.height <= rect.height {
                minFontSize = midFontSize
            } else {
                maxFontSize = midFontSize
            }
        }

        // It might be possible that while loop break with last assignment
        // to `maxFontSize`, which can overflow the available height
        // Using `minFontSize` will be failsafe 
        font = font?.withSize(minFontSize)
        return minFontSize
    }
}
Related