Calculate Font Size to Fit Frame - Core Text - NSAttributedString - iOS
Asked Answered
D

13

25

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);
}
Dulia answered 9/12, 2013 at 10:47 Comment(4)
This custom UILabel is using this. I think this can help https://github.com/vigorouscoding/KSLabelCrocoite
I'm not using a UILabel as they have to be square - this is text being drawn into a Quartz 2D created shape.Dulia
UILabels can be square?Birdt
@Dulia - see my answer. It's really a simple and fast solution.Electronic
E
8

The only way I can see this being possible is to have a system that runs the size calculation then adjusts the size and repeats until it finds the right size.

I.e. set up a bisecting algorithm that goes between certain sizes.

i.e. run it for size 10. Too small. Size 20. Too small. Size 30. Too big. Size 25. Too small. Size 27. Just right, use size 27.

You could even start in hundreds.

Size 100. Too big. Size 50. etc...

Eleni answered 9/12, 2013 at 11:2 Comment(2)
Thanks - I guess the question is how would I know if it is the right size. You are making me think again - maybe what I want to do isn't possible.Dulia
No, buts it's from someone :-)Eleni
H
25

Here is a simple piece of code that will figure out the maximum font size to fit within the bounds of a frame:

UILabel *label = [[UILabel alloc] initWithFrame:frame];
label.text = @"Some text";
float largestFontSize = 12;
while ([label.text sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:largestFontSize]}].width > modifierFrame.size.width)
{
     largestFontSize--;
}
label.font = [UIFont systemFontOfSize:largestFontSize];
Habitual answered 3/10, 2014 at 15:6 Comment(2)
works great! except only for one line of text. i wonder if theres a way to do multi lineMccollum
I know it's an old posting but it can still help some people today and tomorrow. So I am throwing in my 2 cents: to get the font size for multiple lines you can pass the number of lines, then you can do like: ...while ([label.text sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:largestFontSize]}].width > modifierFrame.size.width * numberOfLines)Automotive
E
8

The only way I can see this being possible is to have a system that runs the size calculation then adjusts the size and repeats until it finds the right size.

I.e. set up a bisecting algorithm that goes between certain sizes.

i.e. run it for size 10. Too small. Size 20. Too small. Size 30. Too big. Size 25. Too small. Size 27. Just right, use size 27.

You could even start in hundreds.

Size 100. Too big. Size 50. etc...

Eleni answered 9/12, 2013 at 11:2 Comment(2)
Thanks - I guess the question is how would I know if it is the right size. You are making me think again - maybe what I want to do isn't possible.Dulia
No, buts it's from someone :-)Eleni
E
6

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.

Explanatory answered 28/9, 2016 at 18:16 Comment(3)
This is a great estimation. I have to add a small amount to the rectangle that the text is drawn in to avoid truncation. Apple must be using a different method so a little padding was needed to handle the edge conditions. All in all, I think this is the best solution.Aglow
Here you are considering only width what about height of the frameCalends
This assumes that height is flexible. You can of course also calculate the ratio of height and then take the lower ratio (width vs. height) and derive your bounding box from it and use it for the fontSize.Explanatory
S
5

The currently accepted answer talks of an algorithm, but iOS provides calculations for an NSString object. I would use sizeWithAttributes: of the NSString class.

sizeWithAttributes:

Returns the bounding box size the receiver occupies when drawn with the given attributes.

    - (CGSize)sizeWithAttributes:(NSDictionary *)attributes

Source: Apple Docs - NSString UIKit Additions Reference

EDIT Misinterpreted the question, so this answer is off the mark.

Skep answered 16/5, 2014 at 12:51 Comment(1)
this should be the best answer. As of iOS 7.0, this is the best way to figure out the bounding rect for a string.Pouliot
E
5

Even more easy/faster (but of course approximate) way would be this:

class func calculateOptimalFontSize(textLength:CGFloat, boundingBox:CGRect) -> CGFloat
    {
        let area:CGFloat = boundingBox.width * boundingBox.height
        return sqrt(area / textLength)
    }

We are assuming each char is N x N pixels, so we just calculate how many times N x N goes inside bounding box.

Electronic answered 19/12, 2015 at 9:14 Comment(2)
this answer deserves more upvotes. I am with you that it is a bit of approx but it is fast and fairly accurate depending on use case.Entomophilous
It also works for me. Example usage: ClassName.calculateOptimalFontSize(textLength: CGFloat(quote.count), boundingBox: CGRect(origin: CGPoint(x: 0,y: 0), size: CGSize(width: self.frame.size.width, height: 50)))Demeter
S
4

You could use sizeWithFont :

[myString sizeWithFont:[UIFont fontWithName:@"HelveticaNeue-Light" size:24]   
constrainedToSize:CGSizeMake(293, 10000)] // put the size of your frame

But it is deprecated in iOS 7, so I recommend if working with string in UILabel :

[string sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:17.0f]}];

If you are working with a rect :

CGRect textRect = [text boundingRectWithSize:mySize
                                 options:NSStringDrawingUsesLineFragmentOrigin
                              attributes:@{NSFontAttributeName:FONT}
                                 context:nil];

CGSize size = textRect.size;
Sterilize answered 9/12, 2013 at 10:52 Comment(3)
But I'm working to a fixed frame - sorry if I'm missing something but not sure how this helps ?Dulia
How would I extract font size (float) from the CGSize size ?Dulia
Ok your question is : "How can I know the perfect font size for a specific frame ?", so there is not answer I think, you will have to find it yourself using the code above and an algorithm like @Eleni said.Sterilize
B
3

You can set the UILabel's property adjustsFontSizeToFitWidth to YES as per Apple's documentation

Burge answered 9/12, 2013 at 10:53 Comment(6)
I'm not using a UILabel.Dulia
I meant you can use UILabel to get the font sizeBurge
I've tried this with no luck - it always returns 17 as the font size. UILabel * dummyLabel = [[UILabel alloc] initWithFrame:frameText]; dummyLabel.text = standString; dummyLabel.adjustsFontSizeToFitWidth = YES; UIView * dummyView = [[UIView alloc]initWithFrame:CGRectMake(0, 0, 200, 200)]; [dummyView addSubview:dummyLabel]; float textSize = dummyLabel.font.pointSize;Dulia
try float textSize = dummyLabel.font.xHeight; instead :)Burge
Adjusts font size means it will shrink the font to fit down to the minimum font size or scale whichever is set.Eleni
this is perfectly correct. Idk why it has negative rating. Works totally fine for me +1Bashaw
C
3

Here is code which will do exactly that: calculate optimal font size within some bounds. This sample is in context of UITextView subclass, so it's using its bounds as a "given frame":

func binarySearchOptimalFontSize(min: Int, max: Int) -> Int {
    let middleSize = (min + max) / 2

    if min > max {
        return middleSize
    }

    let middleFont = UIFont(name: font!.fontName, size: CGFloat(middleSize))!

    let attributes = [NSFontAttributeName : middleFont]
    let attributedString = NSAttributedString(string: text, attributes: attributes)

    let size = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)
    let options: NSStringDrawingOptions = [.usesLineFragmentOrigin, .usesFontLeading]
    let textSize = attributedString.boundingRect(with: size, options: options, context: nil)

    if textSize.size.equalTo(bounds.size) {
        return middleSize
    } else if (textSize.height > bounds.size.height || textSize.width > bounds.size.width) {
        return binarySearchOptimalFontSize(min: min, max: middleSize - 1)
    } else {
        return binarySearchOptimalFontSize(min: middleSize + 1, max: max)
    }
}

I hope that helps.

Coats answered 14/11, 2016 at 23:3 Comment(1)
It's not working if the textSize doesn't actually equal the bounds.Cipher
S
3

Here is my solution in swift 4:

private func adjustedFontSizeOf(label: UILabel) -> CGFloat {
    guard let textSize = label.text?.size(withAttributes: [.font: label.font]), textSize.width > label.bounds.width else {
        return label.font.pointSize
    }

    let scale = label.bounds.width / textSize.width
    let actualFontSize = scale * label.font.pointSize

    return actualFontSize
}

I hope it helps someone.

Snarl answered 2/4, 2018 at 7:34 Comment(0)
F
2

I like the approach given by @holtwick, but found that it would sometimes overestimate what would fit. I created a tweak that seems to work well in my tests. Tip: Don't forget to test with really wide letters like "WWW" or even "௵௵௵"

func idealFontSize(for text: String, font: UIFont, width: CGFloat) -> CGFloat {
    let baseFontSize = CGFloat(256)
    let textSize = text.size(attributes: [NSFontAttributeName: font.withSize(baseFontSize)])
    let ratio = width / textSize.width

    let ballparkSize = baseFontSize * ratio
    let stoppingSize = ballparkSize / CGFloat(2) // We don't want to loop forever, if we've already come down to 50% of the ballpark size give up
    var idealSize = ballparkSize
    while (idealSize > stoppingSize && text.size(attributes: [NSFontAttributeName: font.withSize(idealSize)]).width > width) {
        // We subtract 0.5 because sometimes ballparkSize is an overestimate of a size that will fit
        idealSize -= 0.5
    }

    return idealSize
}
Fructose answered 8/6, 2017 at 16:29 Comment(0)
I
2

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
    }
}
Incarnate answered 23/12, 2020 at 5:34 Comment(0)
B
1

This is the code to have dynamic font size changing by the frame width, using the logic from the other answers. The while loop might be dangerous, so please donot hesitate to submit improvements.

float fontSize = 17.0f; //initial font size
CGSize rect;
while (1) {
   fontSize = fontSize+0.1;
   rect = [watermarkText sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:fontSize]}];
    if ((int)rect.width == (int)subtitle1Text.frame.size.width) {
        break;
    }
}
subtitle1Text.fontSize = fontSize;
Bunn answered 28/12, 2014 at 18:19 Comment(2)
Can someone explain also why it got a minus rating? is something wrong in this code?Bunn
I didn't vote you down but I'm pretty sure whoever did it just copied-pasted this snippet without results. The reason is most likely the stopping condition, it needs to be exactly the same width in order to break the cycle. It should be >= (if you're aiming up) or <= (otherwise)Krefeld
R
1

Here's a method that seems to work well for iOS 9 using UITextView objects. You might have to tweet it a bit for other applications.

/*!
 * Find the height of the smallest rectangle that will enclose a string using the given font.
 *
 * @param string            The string to check.
 * @param font              The drawing font.
 * @param width             The width of the drawing area.
 *
 * @return The height of the rectngle enclosing the text.
 */

- (float) heightForText: (NSString *) string font: (UIFont *) font width: (float) width {
    NSDictionary *fontAttributes = [NSDictionary dictionaryWithObject: font
                                                               forKey: NSFontAttributeName];
    CGRect rect = [string boundingRectWithSize: CGSizeMake(width, INT_MAX)
                                       options: NSStringDrawingUsesLineFragmentOrigin
                                    attributes: fontAttributes
                                       context: nil];
    return rect.size.height;
}

/*!
 * Find the largest font size that will allow a block of text to fit in a rectangle of the given size using the system
 * font.
 *
 * The code is tested and optimized for UITextView objects.
 *
 * The font size is determined to ±0.5. Change delta in the code to get more or less precise results.
 *
 * @param string            The string to check.
 * @param size              The size of the bounding rectangle.
 *
 * @return: The font size.
 */

- (float) maximumSystemFontSize: (NSString *) string size: (CGSize) size {
    // Hack: For UITextView, the last line is clipped. Make sure it's not one we care about.
    if ([string characterAtIndex: string.length - 1] != '\n') {
        string = [string stringByAppendingString: @"\n"];
    }
    string = [string stringByAppendingString: @"M\n"];

    float maxFontSize = 16.0;
    float maxHeight = [self heightForText: string font: [UIFont systemFontOfSize: maxFontSize] width: size.width];
    while (maxHeight < size.height) {
        maxFontSize *= 2.0;
        maxHeight = [self heightForText: string font: [UIFont systemFontOfSize: maxFontSize] width: size.width];
    }

    float minFontSize = maxFontSize/2.0;
    float minHeight = [self heightForText: string font: [UIFont systemFontOfSize: minFontSize] width: size.width];
    while (minHeight > size.height) {
        maxFontSize = minFontSize;
        minFontSize /= 2.0;
        maxHeight = minHeight;
        minHeight = [self heightForText: string font: [UIFont systemFontOfSize: minFontSize] width: size.width];
    }

    const float delta = 0.5;
    while (maxFontSize - minFontSize > delta) {
        float middleFontSize = (minFontSize + maxFontSize)/2.0;
        float middleHeight = [self heightForText: string font: [UIFont systemFontOfSize: middleFontSize] width: size.width];
        if (middleHeight < size.height) {
            minFontSize = middleFontSize;
            minHeight = middleHeight;
        } else {
            maxFontSize = middleFontSize;
            maxHeight = middleHeight;
        }
    }

    return minFontSize;
}
Rebel answered 27/7, 2016 at 23:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.