3

I am using drawRect for a text display, calling NSString. I am trying to implement using sizeWithFont to auto resizing font (shrinking) with default font size of 17 and using a loop to reduce the font size by 1 if it does not fit the size of width. Can anyone help me how to implement this? Example would be nice right now I just have the font size set to 17.0

[[self.string displayName] drawAtPoint:CGPointMake(xcoord, ycoord) withFont:[UIFont boldSystemFontOfSize:17.0]];
CGSize size = [[self.patient displayName] sizeWithFont:[UIFont boldSystemFontOfSize:17.0]];
max_current_y = size.height > max_current_y ? size.height : max_current_y;
xcoord = xcoord + 3.0f + size.width;
rmaddy
  • 314,917
  • 42
  • 532
  • 579
user2033092
  • 41
  • 1
  • 5

7 Answers7

12

OK never mind. Here's modified version of the same method that takes NSString for which to return a font:

    -(UIFont*)getFontForString:(NSString*)string
               toFitInRect:(CGRect)rect
                  seedFont:(UIFont*)seedFont{
    UIFont* returnFont = seedFont;
    CGSize stringSize = [string sizeWithAttributes:@{NSFontAttributeName : seedFont}];

    while(stringSize.width > rect.size.width){
        returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
        stringSize = [string sizeWithAttributes:@{NSFontAttributeName : returnFont}];
    }

    return returnFont;
}

Here's how to call it:

NSString* stringToDraw = @"Test 123";

    CGRect rect = CGRectMake(100., 100., 100., 200.);
    UIFont* font = [self getFontForString:stringToDraw toFitInRect:rect seedFont:[UIFont systemFontOfSize:20]];
    [stringToDraw drawInRect:rect withFont:font];

Code is for iOS7+

Alex Reynolds
  • 6,264
  • 4
  • 26
  • 42
puru020
  • 808
  • 7
  • 20
  • do I need to add getFontForString to category? Where exactly do I need to implement those? – user2033092 Feb 11 '13 at 21:09
  • You don't have to add this method to category because I changed it to take NSString. You can add this in any of your utility class say StringHelper. StringHelper class can be singleton. You get the shared StringHelper object and then call this method. Does this help? – puru020 Feb 11 '13 at 21:25
  • thank you so much it works now! had trouble assembling with existing codes – user2033092 Feb 13 '13 at 18:25
  • Hi, just an improvement: changing the code to call the change of the size on the seed font allows a more general approach freeing from the system font constrain. change: FROM `returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];` TO `returnFont = [returnFont fontWithSize:returnFont.pointSize -1];` – Marco Tansini Mar 15 '13 at 11:20
  • Another answer here employs the [False Position numerical Method](http://en.wikipedia.org/wiki/False_position_method#Numerical_analysis) to search for the font size and is a very much better solution. It will converge to a precise fractional font size (instead of just a very approximate integer font size). It uses a very small number of iterations (2-4), rather than the possibly large number of iterations needed for this brute force approach. – Benjohn Jan 16 '14 at 10:19
  • 1
    Fatal flaw: you want `stringSize = [string sizeWithAttributes:@{NSFontAttributeName : returnFont}];` (note `returnFont`) otherwise you end up with an endless loop. – brandonscript Jun 01 '15 at 22:39
9

Trying font sizes with step 1.0 may be very slow. You can tremendously improve the algorithm by making two measures for two different sizes, then using linear approximation to guess the size that will be very close to the right one.

If it turns out not close enough, repeat the calculation using the guessed size instead of one of the previous two until it is good enough or stops changing:

// any values will do, prefer those near expected min and max
CGFloat size1 = 12.0, size2 = 56.0; 
CGFloat width1 = measure_for_size(size1);
CGFloat width2 = measure_for_size(size2);

while (1) {
    CGFloat guessed_size = size1 + (required_width - width1) * (size2 - size1) / (width2 - width1);

    width2 = measure_for_size(guessed_size);
    if ( fabs(guessed_size-size2) < some_epsilon || !is_close_enough(width2, required_width) ) {
        size2 = guessed_size;
        continue;
    }
    // round down to integer and clamp guessed_size as appropriate for your design
    return floor(clamp(guessed_size, 6.0, 24.0));
}

is_close_enough() implementation is completely up to you. Given that text width grows almost linearly of font size, you can simply drop it and just do 2-4 iterations which should be enough.

hamstergene
  • 24,039
  • 5
  • 57
  • 72
  • 2
    This is a **much** better solution than the brute force search of the accepted answer. Its worth noting that `sizeWithFont:` is deprecated as of iOS 7.0. The replacement call `sizeWithAttributes:` is a significant improvement for a search like this because it returns the exact unrounded size. – Benjohn Jan 16 '14 at 10:07
5

I wanted to try to make a version that didn't have to repeatedly check font sizes using a do...while loop. Instead, I assumed that font point sizes were a linear scale, then worked out the size difference between the required frame width and the actual frame width, then adjusted the font size accordingly. Therefore, I ended up with this function:

+ (CGFloat)fontSizeToFitString:(NSString *)string inWidth:(float)width withFont:(UIFont *)font
{
    UILabel *label = [UILabel new];
    label.font = font;
    label.text = string;
    [label sizeToFit];

    float ratio = width / label.frame.size.width;
    return font.pointSize * ratio;
}

Pass in a font of any size, as well as the string and the required width, and it will return you the point size for that font.

I also wanted to take it a bit further and find out the font size for a multi-line string, so that the longest line would fit without a line break:

+ (CGFloat)fontSizeToFitLongestLineOfString:(NSString *)string inWidth:(float)width withFont:(UIFont *)font
{
    NSArray *stringLines = [string componentsSeparatedByString:@"\n"];

    UILabel *label = [UILabel new];
    label.font = font;

    float maxWidth = 0;

    for(NSString *line in stringLines)
    {
        label.text = line;
        [label sizeToFit];
        maxWidth = MAX(maxWidth, label.frame.size.width);
    }

    float ratio = width / maxWidth;
    return font.pointSize * ratio;
}

Seems to work perfectly fine for me. Hope it helps someone else.

jowie
  • 8,028
  • 8
  • 55
  • 94
1

Original poster didn't specify what platform he was working on, but for OSX developers on Mavericks, sizeWithFont: doesn't exist and one should use sizeWithAttributes :

NSSize newSize = [aString sizeWithAttributes:
[NSDictionary dictionaryWithObjectsAndKeys:
           [NSFont fontWithName:@"Arial Rounded MT Bold" size:53.0],NSFontAttributeName,nil
]];
Bertrand Caron
  • 2,525
  • 2
  • 22
  • 49
0

Here's a method which can return you font that will fit in a rect:

-(UIFont*)getFontToFitInRect:(CGRect)rect seedFont:(UIFont*)seedFont{
    UIFont* returnFont = seedFont;
    CGSize stringSize = [self sizeWithFont:returnFont];

    while(stringSize.width > rect.size.width){
        returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
        stringSize = [self sizeWithFont:returnFont];
    }

    return returnFont;
}

You can add this method to a NSString category. You can find more about how to add a category here: http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW2

If you don't want to create a category, you can add this method to one of your utility classes and pass in the string for which you want the font to be returned.

puru020
  • 808
  • 7
  • 20
0

Here is another method, inspired by @puru020 & @jowie answers. Hope it helps someone

-(UIFont *) adjustedFontSizeForString:(NSString *)string forWidth:(float)originalWidth forFont:(UIFont *)font
{
    CGSize stringSize = [string sizeWithFont:font];
    if(stringSize.width <= originalWidth)
    {
        return font;
    }
    float ratio = originalWidth / stringSize.width;
    float fontSize = font.pointSize * ratio;
    return [font fontWithSize:fontSize];
}
Kamran Khan
  • 1,367
  • 1
  • 15
  • 19
0

I modified a bit the solution of @puru020 , added the support for attributes, and improved a bit:

Note: The method should be wrapped in a NSString Category

- (UIFont*)requiredFontToFitInSize:(CGSize)size seedFont:(UIFont*)seedFont attributes:(NSDictionary*)attributes{
   UIFont *returnFont = [UIFont systemFontOfSize:seedFont.pointSize +1];
   NSMutableDictionary *mutableAttributes = attributes.mutableCopy;
   CGSize stringSize;

   do {
    returnFont = [UIFont systemFontOfSize:returnFont.pointSize -1];
    [mutableAttributes setObject:returnFont forKey:NSFontAttributeName];
    stringSize = [self sizeWithAttributes:mutableAttributes];
   } while (stringSize.width > size.width);

   return returnFont;
}
Serluca
  • 2,102
  • 2
  • 19
  • 31