Scaling text to fit on iPhone
Asked Answered
P

3

7

I'm having a bit of trouble working out the "best" way to render text in my application.

My main view consists of a text view, and the design of the application dictates a few things:

  • The (font) size of the text should be dynamic
  • The text frame should be centred vertically in the view
  • Hyphenation should be automatic and only when needed (avoided if possible)

At the moment i'm using a UILabel and the following code to try and guess the best font size to use for the amount of text:

txt  = @"this is just some sample text";

mylabel.font = [self getFontForString:txt];
mylabel.adjustsFontSizeToFitWidth = YES;
mylabel.numberOfLines = 0;
[mylabel setText:txt];

And:

- (UIFont *) getFontForString:(NSString *)txt {
     CGFloat                textLength = txt.length;
     CGFloat                maxFontSize = 71;
     CGFloat                minFontSize = 27;
     CGFloat                newFontSize = 0;

     NSArray                *chunks = [txt componentsSeparatedByString:@" "];
     NSSortDescriptor *sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"length" ascending:NO] autorelease];
     NSArray                *sortedChunks = [chunks sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];

     CGSize                labelSize = theThingLabel.bounds.size;
     CGSize                projectedSize = [[sortedChunks objectAtIndex:0] sizeWithFont:[UIFont boldSystemFontOfSize:maxFontSize]];

     if (projectedSize.width > labelSize.width) {
          CGFloat percentageDifference = ((projectedSize.width - labelSize.width)/labelSize.width)*100;
          if (percentageDifference > 50) {
               newFontSize = ((minFontSize/percentageDifference)*100) - 10;
               if (newFontSize < minFontSize) newFontSize = minFontSize;
          } else {
               newFontSize = ((percentageDifference/maxFontSize)*100) - 10;
               if(newFontSize < (maxFontSize/2)) newFontSize = maxFontSize - abs(newFontSize);
          }
     } else {
          if ( textLength > 11 && textLength < 255) {
               newFontSize = (maxFontSize - ((maxFontSize - minFontSize) * ((textLength- 11) / 100)));
          } else if (textLength <= 11) {
               newFontSize = maxFontSize;
          } else if (textLength >= 255) {
               newFontSize = minFontSize;
          }
     }

     return [UIFont boldSystemFontOfSize:newFontSize];
}

This works, to an extent, but often falls over when the text is a bit on the long side, these two example show it rendering the following strings:

  • "short amount of text"
  • "a substantially longer amount of text which i still want to render nicely."

short text longer text

As you can see in the second example (with far longer text) there are a number of issues:

  • The initial widow
  • The dropped y
  • The missing "nicely."

So with all this in mind, what are my options, I'm open to moving to using coretext if this is the right solution, but have no idea where to start, it's also possible I've made a mistake which I just can't see in my "font size guessing" code.

Petromilli answered 16/11, 2010 at 22:28 Comment(2)
Had a similar issue and solved it to some degree. This solution doesn't do any hyphenation but does solve the issue that sizeWithFont internally truncates the string before calculating the size.Makeshift
I have posted my answer and is available at the following link [Calculae Font Size to fit in a Rect with a NSString][1] [1]: #4091615Gaelan
G
3

The following method will be use full to get the font size for specific string for specific rectangle (area).

-(float) maxFontSizeThatFitsForString:(NSString*)_string inRect:(CGRect)rect withFont:(NSString *)fontName onDevice:(int)device {   
// this is the maximum size font that will fit on the device
float _fontSize = maxFontSize;
float widthTweak;

// how much to change the font each iteration. smaller
// numbers will come closer to an exact match at the 
// expense of increasing the number of iterations.
float fontDelta = 2.0;

// sometimes sizeWithFont will break up a word 
// if the tweak is not applied. also note that 
// this should probably take into account the
// font being used -- some fonts work better
// than others using sizeWithFont.
if(device == IPAD)
    widthTweak = 0.2;
else
    widthTweak = 0.2;

CGSize tallerSize = CGSizeMake(rect.size.width-(rect.size.width*widthTweak), 100000);
CGSize stringSize = CGSizeZero;

    if([[UIDevice currentDevice].systemVersion floatValue]>=7.0){
        NSDictionary *stringAttributes = [NSDictionary dictionaryWithObject:[UIFont boldSystemFontOfSize:17] forKey: NSFontAttributeName];
        stringSize = [_string boundingRectWithSize: tallerSize options:NSStringDrawingUsesLineFragmentOrigin attributes:stringAttributes context:nil].size;
    }
    else{
        stringSize = [_string sizeWithFont:[UIFont fontWithName:fontName size:_fontSize] constrainedToSize:tallerSize];
    }

while (stringSize.height >= rect.size.height)
{       
    _fontSize -= fontDelta;
    stringSize = [_string sizeWithFont:[UIFont fontWithName:fontName size:_fontSize] constrainedToSize:tallerSize];
}

return _fontSize;
}

Use this and get the font size and assign to the label.

Gertrudegertrudis answered 2/12, 2010 at 10:34 Comment(1)
if(device == IPAD) widthTweak = 0.2; else widthTweak = 0.2; - are we allowed to post SO answers to thedailywtf.com? ;-)Globe
C
1

One thing I have found useful is a little function that takes a NSString, a UIFont and a CGSize, returning a CGFloat representing the largest font size for that string that will fit in the CGSize passed - it uses sizeWithFont on successively smaller point sizes until the size returned fits within the CGSize argument. You can pass CGFLOAT_MAX as either x or y if you don't care about one dimension, like when you are checking width of a line and will check height later on the whole string. You define max and min font sizes of course.

I would begin by separating the string into an array of words using componentsSeparatedByString. Possibly you want to define a word that may be hyphenated as a word that is some multiple of the next largest word when all words are rendered at the same font size, so you create that array for some arbitrary font size and you have a matching array of relative widths (or maybe a dictionary where the word is the key and the width the value). You need to scan for and separate any such word(s) into two words (one containing the hyphen) before proceeding. Next you need to find the font size at which all the words fit into your size constraint, in the example you have shown that is width, using the function mentioned above.

When the candidate font size is known you check for your other constraints such as widows - probably you define these in terms of the positions they may not occur (the start for one) and the proportion of line width that makes a widow. If you find a problem you combine words in the array to remove the widow and recalculate a new candidate font size - doing this might cause you to end up with hyphenation on the first line or a smaller font overall but if your text is "a disproportionally long start to a group of small words" then you may have no choice.

The problem of "nicely" disappearing isn't so hard, you just need to double-check the height of the rendered text - probably you could use sizeWithFont:constrainedToSize once you have completed the procedures above as your final check. If it fails, reduce maximum candidate font size and start over.

So:

candidateFontSize = CGFLOAT_MAX;
while(stillRendering)

  break string into array of words
  make array of word widths
  check for and divide hyphenation candidates

  for each word
    if max font size returned for rendered word in size constraint < canddiateFontSize
      candidateFontSize = max font size returned

  for each word
    measure each word and record rendered size in candidateFontSize

  stillRendering = NO;

  for each word 
    check each word for problems such as widows and solve
    if problem found
      stillRendering = YES;

  check entire string using sizeWithFont:constrainedToSize using width, CGFLOAT_MAX

  if height is too large
    stillRendering = YES;
    candidateFontSize--;

It's only a start but it should be workable from there.

Coppinger answered 17/11, 2010 at 0:33 Comment(0)
K
0
CGSize expectedLabelSize = [yourString sizeWithFont:yourLable.font];

        //adjust the label the the new height.
    CGRect newFrame = yourLable.frame;
    newFrame.size.width = expectedLabelSize.width;
    newFrame.origin.x = x;
    yourLable.frame = newFrame;
Kearse answered 23/11, 2010 at 7:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.