Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

boundingRectWithSize for NSAttributedString returning wrong size

Looks like you weren't providing the correct options. For wrapping labels, provide at least:

CGRect paragraphRect =
  [attributedText boundingRectWithSize:CGSizeMake(300.f, CGFLOAT_MAX)
  options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
  context:nil];

Note: if the original text width is under 300.f there won't be line wrapping, so make sure the bound size is correct, otherwise you will still get wrong results.


For some reason, boundingRectWithSize always returns wrong size. I figured out a solution. There is a method for UItextView -sizeThatFits which returns the proper size for the text set. So instead of using boundingRectWithSize, create an UITextView, with a random frame, and call its sizeThatFits with the respective width and CGFLOAT_MAX height. It returns the size that will have the proper height.

   UITextView *view=[[UITextView alloc] initWithFrame:CGRectMake(0, 0, width, 10)];   
   view.text=text;
   CGSize size=[view sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)];
   height=size.height; 

If you are calculating the size in a while loop, do no forget to add that in an autorelease pool, as there will be n number of UITextView created, the run time memory of the app will increase if we do not use autoreleasepool.


Ed McManus has certainly provided a key to getting this to work. I found a case that does not work

UIFont *font = ...
UIColor *color = ...
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                     font, NSFontAttributeName,
                                     color, NSForegroundColorAttributeName,
                                     nil];

NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString: someString attributes:attributesDictionary];

[string appendAttributedString: [[NSAttributedString alloc] initWithString: anotherString];

CGRect rect = [string boundingRectWithSize:constraint options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:nil];

rect will not have the correct height. Notice that anotherString (which is appended to string) was initialized without an attribute dictionary. This is a legitimate initializer for anotherString but boundingRectWithSize: does not give an accurate size in this case.


My final decision after long investigation:
- boundingRectWithSize function returns correct size for uninterrupted sequence of characters only! In case string contains spaces or something else (called by Apple "Some of the glyphs" ) - it is impossible to get actual size of rect needed to display text!
I have replaced spaces in my strings by letters and immediately got correct result.

Apple says here: https://developer.apple.com/documentation/foundation/nsstring/1524729-boundingrectwithsize

"This method returns the actual bounds of the glyphs in the string. Some of the glyphs (spaces, for example) are allowed to overlap the layout constraints specified by the size passed in, so in some cases the width value of the size component of the returned CGRect can exceed the width value of the size parameter."

So it is necessary to find some another way to calculate actual rect...


After long investigation process solution finally found!!! I am not sure it will work good for all cases related to UITextView, but main and important thing was detected!

boundingRectWithSize function as well as CTFramesetterSuggestFrameSizeWithConstraints (and many other methods) will calculate size and text portion correct when correct rectangle used. For example - UITextView has textView.bounds.size.width - and this value not actual rectangle used by system when text drawing on UITextView.

I found very interesting parameter and performed simple calculation in code:

CGFloat padding = textView.textContainer.lineFragmentPadding;  
CGFloat  actualPageWidth = textView.bounds.size.width - padding * 2;

And magic works - all my texts calculated correct now! Enjoy!


Swift four version

let string = "A great test string."
let font = UIFont.systemFont(ofSize: 14)
let attributes: [NSAttributedStringKey: Any] = [.font: font]
let attributedString = NSAttributedString(string: string, attributes: attributes)
let largestSize = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)

//Option one (best option)
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
let textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(), nil, largestSize, nil)

//Option two
let textSize = (string as NSString).boundingRect(with: largestSize, options: [.usesLineFragmentOrigin , .usesFontLeading], attributes: attributes, context: nil).size

//Option three
let textSize = attributedString.boundingRect(with: largestSize, options: [.usesLineFragmentOrigin , .usesFontLeading], context: nil).size

Measuring the text with the CTFramesetter works best as it provides integer sizes and handles emoji's and other unicode characters well.


I didn't have luck with any of these suggestions. My string contained unicode bullet points and I suspect they were causing grief in the calculation. I noticed UITextView was handling the drawing fine, so I looked to that to leverage its calculation. I did the following, which is probably not as optimal as the NSString drawing methods, but at least it's accurate. It's also slightly more optimal than initialising a UITextView just to call -sizeThatFits:.

NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, CGFLOAT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:formattedString];
[textStorage addLayoutManager:layoutManager];

const CGFloat formattedStringHeight = ceilf([layoutManager usedRectForTextContainer:textContainer].size.height);

Turns out that EVERY part of an NSAttributedString must have a dictionary set with at least NSFontAttributeName and NSForegroundColorAttributeName set, if you wish boundingRectWithSize to actually work!

I don't see that documented anywhere.