20

When using [NSString boundingRectWithSize:options:attributes] the size of the rect that is returned is taller than I would expect for certain strings. The height returned appears to represent the maximum possible height of a string with the given attributes, rather than the height of the string itself.

Assuming the same attributes and options, the height returned for the string "cars" is the same height returned for the string "ÉTAS-UNIS" (note the accent on the E).

I would have expected boundingRectWithSize to only consider the characters in the given string, which in my opinion would have it return a shorter height for the string "cars".

In the attached screenshots, I've filled the rect returned from boundingRectWithSize and outlined in red what I would have assumed the bounding rect should have been. The width of the rect is pretty much as I would expect but the height is considerably taller than I would have expected. Why is that?

Bounding Rect Example

Sample code:

NSRect boundingRect = NSZeroRect;
NSSize constraintSize = NSMakeSize(CGFLOAT_MAX, 0);

NSString *lowercaseString = @"cars";
NSString *uppercaseString = @"ÉTAS-UNIS";
NSString *capitalizedString = @"Japan";

NSFont *drawingFont = [NSFont fontWithName:@"Georgia" size:24.0];
NSDictionary *attributes = @{NSFontAttributeName : drawingFont, NSForegroundColorAttributeName : [NSColor blackColor]};

boundingRect = [lowercaseString boundingRectWithSize:constraintSize options:0 attributes:attributes];
NSLog(@"Lowercase rect: %@", NSStringFromRect(boundingRect));

boundingRect = [uppercaseString boundingRectWithSize:constraintSize options:0 attributes:attributes];
NSLog(@"Uppercase rect: %@", NSStringFromRect(boundingRect));

boundingRect = [capitalizedString boundingRectWithSize:constraintSize options:0 attributes:attributes];
NSLog(@"Capitalized rect: %@", NSStringFromRect(boundingRect));

Output:

Lowercase rect: {{0, -6}, {43.1953125, 33}}
Uppercase rect: {{0, -6}, {128.44921875, 33}}
Capitalized rect: {{0, -6}, {64.5, 33}}
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
kennyc
  • 5,490
  • 5
  • 34
  • 57
  • Please check the answer for the similar question here- [http://stackoverflow.com/a/27289303/591811][1] [1]: http://stackoverflow.com/a/27289303/591811 – shoan Dec 04 '14 at 08:30

4 Answers4

29

You might want to use NSStringDrawingUsesDeviceMetrics in the options. From the docs:

NSStringDrawingUsesDeviceMetrics

Use the image glyph bounds (instead of the typographic bounds) when computing layout.

Community
  • 1
  • 1
omz
  • 53,243
  • 5
  • 129
  • 141
  • I was all but certain I'd tried that flag but I guess I'd always tried it alongside the `NSStringDrawingUsesLineFragmentOrigin` flag, which didn't produce the results I wanted. Used alone, it does. Thank you. It seems remarkably difficult to draw a perfectly sized rect behind some truncated text. Asking for the bounding rect with `NSStringDrawingUsesLineFragmentOrigin` includes "padding", but that flag appears needed for `NSStringDrawingTruncatesLastVisibleLine` to be used. – kennyc Jan 22 '14 at 20:09
5

@omz still gets credit for making this work for me. His answer made me look around CoreText some more since I assume something like boundingRectWithSize ultimately calls a CoreText function.

In Session 226 from WWDC 2012 there was an entire section devoted to calculating the metrics of a line and, to my surprise, they talked about a new CoreText function called CTLineGetBoundsWithOptions.

As far as I can tell, that method is only documented in CTLine.h and in the CoreText Changes document. It certainly doesn't come up (for me) when doing a normal search in the documentation.

But it appears to work and in my testing it returns the exact same result as boundingRectWithSize for all the fonts installed on my system. Even better, is that it appears to be almost 2x faster than boundingRectWithSize.

As the WWDC video mentions, it's a bit obscure why you'd need to calculate the bounds of a string without taking things like line height into account, but if you do need to do that, then I think this might be the best method to use. As always, YMMV.

Rough sample code:

NSFont *font = [NSFont systemFontOfSize:13.0];
NSDictionary *attributes = @{(__bridge NSString *)kCTFontAttributeName : font};
NSAttributedString *attributeString = [[NSAttributedString alloc] initWithString:self.text attributes:attributes];

CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributeString);
CGRect bounds = CTLineGetBoundsWithOptions(line, kCTLineBoundsUseGlyphPathBounds);

CFRelease(line);

return NSRectFromCGRect(bounds);
kennyc
  • 5,490
  • 5
  • 34
  • 57
  • `CTLineGetBoundsWithOptions` is also mentioned in the "what's new" document for OS 10.8. – JWWalker Feb 04 '14 at 23:31
  • 1
    Thank you for the mention of CTLineGetBoundsWithOptions returns the correct text size while boundingRectWithSize: options: context: gives me double the height in iOS 8. Totally weird. Now it's time to figure out how to have the text wrap. – Ben Sep 29 '14 at 02:49
  • I'm seeing iOS8 double the height of calculated text, too, regardless of the options I set. CTLineGetBoundsWithOptions works like a charm. – u2Fan Dec 22 '14 at 20:40
  • this is giving for width. Not Height. – Saad Apr 28 '16 at 11:18
0

I tried the boundingRectWithSize function, but it didn't work for me.

What did work was sizeThatFits

Usage:

CGSize maxSize = CGSizeMake(myLabel.frame.size.width, CGFLOAT_MAX)

CGSize size = [myLabel sizeThatFits:maxSize];
Shaked Sayag
  • 5,712
  • 2
  • 31
  • 38
0

Swift 3+ solution based on omz's answer:

let string: NSString = "test"
let maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
let boundingRect = string.boundingRect(with: maxSize, options: .usesDeviceMetrics, attributes: <string attributes>)
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223