17

I'm trying to use the attributed string API of iOS 6 to calculate the size of text and shrink the font size if necessary. However, I can't get it to work as the documentation says.

NSString *string = @"This is a long text that doesn't shrink as it should";

NSStringDrawingContext *context = [NSStringDrawingContext new];
context.minimumScaleFactor = 0.5;

UIFont *font = [UIFont fontWithName:@"SourceSansPro-Bold" size:32.f];

NSMutableParagraphStyle *paragraphStyle = [NSMutableParagraphStyle new];
paragraphStyle.lineBreakMode = NSLineBreakByClipping;

NSDictionary *attributes = @{ NSFontAttributeName: font,
                              NSParagraphStyleAttributeName: paragraphStyle };

NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:self.title attributes:attributes];

CGRect rect = [attributedString boundingRectWithSize:CGSizeMake(512.f, 512.f) options:NSStringDrawingUsesLineFragmentOrigin context:context];

NSLog(@"rect: %@, context: %@", NSStringFromCGRect(rect), context.debugDescription);

But the text doesn't shrink and is truncated. actualScaleFactor is always 1. The log results are:

rect:{{0, 0}, {431.64801, 80.447998}}, context:<NSStringDrawingContext: 0x14e85770> minimumScaleFactor:0.500000 minimumTrackingAdjustment:0.000000 actualScaleFactor:1.000000 actualTrackingAdjustment:0.000000 totalBounds:{{0, 0}, {431.64801, 80.447998}}

The result is the same if I use the actual drawing method and not the measuring method. If I remove the paragraph style, it makes the text wrap and doesn't shrink it. If I remove the paragraph style AND I choose a size that only allows one line of text, the text is truncated too instead of being shrunk. What is wrong? There is very little documentation or online resources dealing with NSStringDrawingContext. And I'm trying to avoid the use of sizeWithFont:minFontSize:actualFontSize:forWidth:lineBreakMode: which is deprecated in iOS 7.

Guillaume
  • 4,331
  • 2
  • 28
  • 31
  • 2
    I have the same problem and just can't get it to work... – Pascal Jul 26 '13 at 20:25
  • I ran into the same problem on iOS 8. actualScaleFactor is not updated is context is used for boundingRectWithSize:options:attributes::context: or with drawWithRect:options:attributes:context:. – Tore Olsen Feb 23 '15 at 15:10

3 Answers3

10

NSStringDrawingContext's minimumScaleFactor appears to be broken in iOS 7.

As far as I can make out, even in iOS 6, it didn't work for drawing; it worked for measuring, so you could work out what would happen in a context where it does work for drawing, like a UILabel. That way, you know the correct minimum height for the label.

Or, you could use the resulting scale factor to shrink the text yourself, in the knowledge that now it will fit.

Example:

- (void)drawRect:(CGRect)rect
{
    // rect is 0,0,210,31

    NSMutableAttributedString* s = 
        [[NSMutableAttributedString alloc] initWithString: @"This is the army Mister Jones."];
    [s addAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"GillSans" size:20]} 
        range:NSMakeRange(0,s.length)];

    NSMutableParagraphStyle* para = [[NSMutableParagraphStyle alloc] init];
    para.lineBreakMode = NSLineBreakByTruncatingTail;
    [s addAttributes:@{NSParagraphStyleAttributeName:para} 
        range:NSMakeRange(0,s.length)];

    NSStringDrawingContext* con = [[NSStringDrawingContext alloc] init];
    con.minimumScaleFactor = 0.5;

    CGRect result = 
        [s boundingRectWithSize:rect.size 
            options:NSStringDrawingUsesLineFragmentOrigin context:con];
    CGFloat scale = con.actualScaleFactor;
    // ...(could have a check here to see if result fits in target rect)...

    // fix font to use scale factor, and draw
    [s addAttributes:@{NSFontAttributeName:[UIFont fontWithName:@"GillSans" size:20*scale]} 
        range:NSMakeRange(0,s.length)];
    [s drawWithRect:rect options:NSStringDrawingUsesLineFragmentOrigin context:nil];

}

In iOS 6, scale is about 0.85 and you can use it as shown to shrink the text. But in iOS 7, scale remains at 1, suggesting that no shrinkage is happening and this feature of NSStringDrawingContext is now useless. I can't tell whether that's a bug or whether the feature has been deliberately abandoned.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Why don't you just pass context to drawWithRect: instead of shrinking text manually? – pronebird Oct 30 '13 at 17:39
  • 4
    @Andy Did you read what I said? It's broken. Broken. It doesn't work. Try it yourself. That's why I'm showing the OP a possible workaround. Workarounds are for things that are broken. You know, bugs. If it were possible to pass the context to `drawWithRect:`, the OP wouldn't be asking this question and I wouldn't be answering it. – matt Oct 30 '13 at 18:34
  • yes right, sorry, I am bit rushed to reply. It still doesn't work on iOS 7.0.3 :/ – pronebird Oct 30 '13 at 22:28
  • It's still broken on iOS 14.5 – Jonny Feb 09 '21 at 08:11
  • @Jonny Yes, every year I refile the same bug report... – matt Feb 09 '21 at 18:59
  • @matt is there alternative way to get actual scale factor? – Den Andreychuk Nov 07 '22 at 19:44
6

After googling for a long time I did not find a solution working under iOS7. Right now I use the following workaround, knowing that it is very ugly. I render a UILabel in memory, take a screenshot and draw that. UILabel is able to shrink the text correctly.

But perhaps someone finds it useful.

    UILabel *myLabel = [[UILabel alloc] initWithFrame:myLabelFrame];
    myLabel.font = [UIFont fontWithName:@"HelveticaNeue-BoldItalic" size:16];
    myLabel.text = @"Some text that is too long";
    myLabel.minimumScaleFactor = 0.5;
    myLabel.adjustsFontSizeToFitWidth = YES;
    myLabel.backgroundColor = [UIColor clearColor];

    UIGraphicsBeginImageContextWithOptions(myLabelFrame.size, NO, 0.0f);
    [[myLabel layer] renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    [screenshot drawInRect:myLabel.frame];
Shingoo
  • 826
  • 10
  • 26
  • What is the purpose of `[screenshot drawInRect:myLabel.frame]`, because without it I seem to get the same result? And, how can I draw the text at a certain rectangle inside a much larger graphics context? – meaning-matters Jul 21 '23 at 14:29
0

Just wanted to post this solution as I have been battling with this for a couple hours. I could never get the text area to scale down and would always have the last lines of text cut off.

The solution for me was to not add a context and just set it to nil. I got this when looking at the example on this site. https://littlebitesofcocoa.com/144-drawing-multiline-strings

Note after getting the size the box would draw at using

pageStringSize = [myString boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attrsDictionary context:nil];

I still needed to loop through scaling down the font manually:

while (pageStringSize.size.height > 140 && scale > 0.5) {
            scale = scale - 0.1;
            font = [UIFont fontWithName:@"Chalkboard SE" size:24.0 *scale];
            attrsDictionary = [NSDictionary dictionaryWithObjectsAndKeys:font, NSFontAttributeName,[NSNumber numberWithFloat:1.0], NSBaselineOffsetAttributeName, nil];

            pageStringSize = [myString boundingRectWithSize:maxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attrsDictionary context:nil];

        }
JonP
  • 1
  • 1