boundingRectWithSize for NSAttributedString returning wrong size
Asked Answered
Y

25

163

I am trying to get the rect for an attributed string, but the boundingRectWithSize call is not respecting the size I pass in and is returning a rect with a single line height as opposed to a large height (it is a long string). I have experimented by passing in a very large value for the height and also 0 as in the code below, but the rect returned is always the same.

CGRect paragraphRect = [attributedText boundingRectWithSize:CGSizeMake(300,0.0)
  options:NSStringDrawingUsesDeviceMetrics
  context:nil];

Is this broken, or do I need to do something else to have it returned a rect for wrapped text?

Yetac answered 29/11, 2012 at 7:48 Comment(2)
Do you happen to have a paragraph style with truncating/clipping lineBreakMode?Hines
if you are reading this because UILabel measuring/wrapping to an incorrect width take a look at #46200527. specifically, setting NSAllowsDefaultLineBreakStrategy to false at app launch.Fighterbomber
D
328

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.

Deannedeans answered 14/3, 2013 at 1:48 Comment(15)
Everyone needs to look at this answer, because it works. Perhaps clearer documentation is needed for this method and attributed strings in general; it's not obvious where you need to look.Thebaid
Attention! It does not always work! Sometimes the returned width is larger than the width of the size parameter. Even Apples method documentation states: "The constraints you specify in the size parameter are a guide for the renderer for how to size the string. However, the actual bounding rectangle returned by this method can be larger than the constraints if additional space is needed to render the entire string."Kimberlykimberlyn
The API for NSAttributedString and boundingRectWithSize is absolutely shocking.Ranice
Another gotcha is that the height (and presumably the width) returned in the paragraphRect is almost always a fractional value. So it may come out saying 141.3 as the height. You need use the result from ceilf(paragraphRect.size.height) so that it rounds up. I forget this all the time and wonder why my labels are still clipping.Fireweed
I always wrap my boundingRectWithSize: calculations in CGRectIntegral() which will CGRectIntegral rounds the rectangle’s origin downward and its size upward to the nearest whole integers, in this case rounding up the height and width to ensure no clipping occurs if the height or width a fractional value.Kubis
sometime this trick doesn't work. Doesn't work for me for some strings.Mildredmildrid
Guys, don't forget about the Apple statement: In iOS 7 and later, this method returns fractional sizes (in the size component of the returned CGRect); to use a returned size to size views, you must use raise its value to the nearest higher integer using the ceil function.Documentary
Why would that bitwise OR work? NSStringDrawingOptions is an enum. Uses the NS_ENUM macro. I don't see why that would do anything, and in Swift it doesn't allow you to OR them. It's confusing that the API asks for "options", but it's not a bitmask.Paradisiacal
Wrapping it up in CGRectIntegral worked great. Good call, @KubisKaolinite
@BenKane, NSStringDrawingOptions uses NS_OPTIONS, at least is does now. Haven't checked back to see if and when it changed. But I can't imagine it was ever not a bitmask.Vitkun
Please include @Fireweed comment!Ultrafilter
This method is deprecated on 10.11.Mastic
How would i write this in swift?Shrovetide
For those like me which that answer seems to not work, you need to check the width you are using to calculate the size. If it is not the right width you are going to get a wrong heightImpenetrability
I feel like I tried everything to make boundingRectWithSize: work, but it just didn't. But [this] (https://mcmap.net/q/23821/-how-to-get-height-for-nsattributedstring-at-a-fixed-width) ended up working. It's lifted straight from (non-outdated) Apple docs, so I have high confidence that it's reliable.Trophy
S
55

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.

Shinbone answered 4/12, 2014 at 8:25 Comment(10)
This works. The other ways I tried I could never get to work. I think my problem stems from the complexity of the attributed string I was assigning. It was coming from an RTF file and used a variety of fonts, line spacing, even shadows, and despite using UsesLineFragmentOrigin and UsesFontLeading I could never get a result that was sufficiently tall, and the problem was worst the longer the attributed string got. My guess is that for relatively simple strings the boundingRectWithSize method may work. They really need a better method of working with this, imo.Harlotry
don't you think it is overkill to create a UITextView as many time as heightForRowAtIndexPath method get called? it takes timeBardo
I agree with you @János, but this was the only solution which worked for me.Shinbone
btw I asked for proper solution: #32496244Bardo
this is the only solution that works. This problem has 9 years and Apple apparently do not want to solve this or their engineers are to weak to figure out a solution for this. We are talking here about iOS 11, still carrying the same bug.Archenemy
This is the only solution that worked for me. Thanks.Terbia
Or don't allocate the textview multiple times ?Peraea
In my case I should do the calculation on background thread, and usage of UI elements is not allowedWhoreson
Yes only this worked , boundingRect is buggy with complex attributedStringSinglefoot
It could be necessary to set view.textContainerInset = UIEdgeInsetsZero or an appropriate insetNonjoinder
B
32

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.

Balthazar answered 14/6, 2013 at 21:9 Comment(6)
Thank you! 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.Dewayne
Note that it also appears that the different NSAttributedStrings that are combined to make a single one must all use the SAME dictionary (and thus the same font) in order for boundingRectWithSize to work correctly!Dewayne
This solution is very similar to the one I use for my UILabel category for "shrink to fit". The key for me to get this to work was to create the bounding rectangle with FLT_MAX height. Without that, I would seem to get a rectangle for just one line.Itinerate
+1s all round. This really got me, so thanks for working it out guys.Playgoer
I had this problem. I used spaces and newlines in NSMutableAttributedString without attributes and I was getting wrong size.Narrative
@Ben Wheeler, you need to get your comments submitted as an answer so we can upvote it and get them the attention they deserve!Cookshop
M
30

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!

Mcanally answered 19/9, 2014 at 19:31 Comment(6)
yeap, I noticed that too, it does not keep the constrains for width, it always comes out bigger. That is really not cool, they deprecated working method and now we have to deal with this crap.Pyx
You sir, are my new hero! This was driving me crazy. I'm setting the textContainers insets, so had to use textView.textContainer.size.width instead of textView.bounds.size.witdh.Massacre
I couldn't up vote this enough. So weird that spaces aren't considered in bounding calculations.Goodoh
Replacing the spaces with some dummy character like "3" returns the exact heightSlipperwort
if you are reading this because UILabel measuring/wrapping to an incorrect width take a look at #46200527. specifically, setting NSAllowsDefaultLineBreakStrategy to false at app launch.Fighterbomber
this saved my career and my relationship with the QA team. I owe you a beer man!!Autolycus
S
24

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.

Selfpronouncing answered 4/7, 2017 at 4:17 Comment(2)
The "best option" doesn't seem to work anymore, but "Option three" does.Selfseeker
Option 2 here did it for me... maybe converting to NSString? Option 3 was my original solution but calculated height there was always for one line... done this 10's of times in the past and never encountered that...Cabasset
F
12

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);
Failsafe answered 28/9, 2015 at 6:31 Comment(1)
It may not have been the unicode dots, it may have been spaces at the end of your string. See the answer above: https://mcmap.net/q/23796/-boundingrectwithsize-for-nsattributedstring-returning-wrong-size.Adelaadelaida
D
10

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.

Dewayne answered 10/6, 2017 at 18:25 Comment(1)
Thanks, this was the solution for me, except I found that only NSFontAttributeName was necessary, not NSForegroundColorAttributeNamePhysicist
C
9

In case you'd like to get bounding box by truncating the tail, this question can help you out.

CGFloat maxTitleWidth = 200;

NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
paragraph.lineBreakMode = NSLineBreakByTruncatingTail;

NSDictionary *attributes = @{NSFontAttributeName : self.textLabel.font,
                             NSParagraphStyleAttributeName: paragraph};

CGRect box = [self.textLabel.text
              boundingRectWithSize:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)
              options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
              attributes:attributes context:nil];
Cripple answered 6/7, 2014 at 0:22 Comment(0)
B
9

I've found that the preferred solution does not handle line breaks.

I've found this approach works in all cases:

UILabel* dummyLabel = [UILabel new];
[dummyLabel setFrame:CGRectMake(0, 0, desiredWidth, CGFLOAT_MAX)];
dummyLabel.numberOfLines = 0;
[dummyLabel setLineBreakMode:NSLineBreakByWordWrapping];
dummyLabel.attributedText = myString;
[dummyLabel sizeToFit];
CGSize requiredSize = dummyLabel.frame.size;
Bryannabryansk answered 13/11, 2015 at 16:32 Comment(1)
In my case I should do the calculation on background thread, and usage of UI elements is not allowedWhoreson
R
7

@warrenm Sorry to say that framesetter method didn't work for me.

I got this.This function can help us to determine the frame size needed for a string range of an NSAttributedString in iphone/Ipad SDK for a given Width :

It can be used for a dynamic height of UITableView Cells

- (CGSize)frameSizeForAttributedString:(NSAttributedString *)attributedString
{
    CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    CGFloat width = YOUR_FIXED_WIDTH;

    CFIndex offset = 0, length;
    CGFloat y = 0;
    do {
        length = CTTypesetterSuggestLineBreak(typesetter, offset, width);
        CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(offset, length));

        CGFloat ascent, descent, leading;
        CTLineGetTypographicBounds(line, &ascent, &descent, &leading);

        CFRelease(line);

        offset += length;
        y += ascent + descent + leading;
    } while (offset < [attributedString length]);

    CFRelease(typesetter);

    return CGSizeMake(width, ceil(y));
}

Thanks to HADDAD ISSA >>> http://haddadissa.blogspot.in/2010/09/compute-needed-heigh-for-fixed-width-of.html

Ritzy answered 27/3, 2014 at 11:17 Comment(3)
Doesn't work for me on any device (iPhone 4 and 5). Both on device with iOS7.1.2 and 4s Simulator with iOS8.3 :(Build
This need any import?Roadblock
@KaranAlangat yes #import <CoreText/CoreText.h>Roadblock
L
5

Update July 2022

After many more trial and error and getting feedback from other answers, specifically the ones pointing out to use NSString.DrawingOptions.usesDeviceMetrics, I found out that this option is definitely a game changer, though is not enough on itself.

Using .deviceMetrics returns the correct height, but it won't fit properly on a UILabel nor on a NSTextField on some cases.

The only way I was able to make it fit on all cases was using a CATextLayer. Which is available for both iOS and macOS.

Example

let attributedString = NSAttributedString(string: "my string")
let maxWidth = CGFloat(300)
let size = attributedString.boundingRect(
                with: .init(width: maxWidth,
                            height: .greatestFiniteMagnitude),
                options: [
                    .usesFontLeading,
                    .usesLineFragmentOrigin,
                    .usesDeviceMetrics])

let textLayer = CATextLayer()
textLayer.frame = .init(origin: .zero, size: size)
textLayer.contentsScale = 2 // for retina
textLayer.isWrapped = true // for multiple lines
textLayer.string = attributedString

Then you can add the CATextLayer to any NSView/UIView.

macOS

let view = NSView()
view.wantsLayer = true
view.layer?.addSublayer(textLayer)

iOS

let view = UIView()
view.layer.addSublayer(textLayer)

Original answer February 2021

Many of the answers here are great, David Rees summarises the options nicely.

But sometimes when there are special characters or multiple white spaces the size seemed to always be wrong.

Example of a not working string (for me):

"hello    .   .  world"

What I found out is that setting the kern of the NSAttributedString to 1 helps returning the right size.

Like this:

NSAttributedString(
    string: "some string",
    attributes: [
        .font: NSFont.preferredFont(forTextStyle: .body), 
        .kern: 1])
Lawry answered 20/2, 2021 at 19:17 Comment(0)
G
4

I have had the same problem with not getting an accurate size using these techniques and I've changed my approach to make it work.

I have a long attributed string which I've been trying to fit into a scroll view so that it shows properly without being truncated. What I did to make the text work reliably was to not set the height at all as a constraint and instead allowed the intrinsic size to take over. Now the text is displayed correctly without being truncated and I do not have to calculate the height.

I suppose if I did need to get the height reliably I would create a view which is hidden and these constraints and get the height of the frame once the constraints are applied.

Gauze answered 10/10, 2013 at 15:14 Comment(1)
Can you add a code sample to demonstrate this? Thx.Unstep
V
3

Im a little late to the game - but I have been trying to figure out a way that works to find the bounding box that will fit around an attributed string to make a focus ring like editing a file in Finder does. everything I had tried failed when there are spaces at the end of the string or multiple spaces inside the string. boundingRectWithSize fails miserably for this as well as CTFramesetterCreateWithAttributedString.

Using a NSLayoutManager the following code seems to do the trick in all the cases I have found so far and returns a rect that perfectly bounds the string. Bonus: if you select the text the edges of the selection go right up to the bounds of the rect returned. The code below uses the layoutManager from a NSTextView.

NSLayoutManager* layout = [self layoutManager];
NSTextContainer* container = [self textContainer];

CGRect focusRingFrame = [layout boundingRectForGlyphRange:NSMakeRange(0, [[self textStorage] length]) inTextContainer:container];
Vallievalliere answered 12/1, 2016 at 14:48 Comment(0)
S
2
textView.textContainerInset = UIEdgeInsetsZero;
NSString *string = @"Some string";
NSDictionary *attributes = @{NSFontAttributeName:[UIFont systemFontOfSize:12.0f], NSForegroundColorAttributeName:[UIColor blackColor]};
NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
[textView setAttributedText:attributedString];
CGRect textViewFrame = [textView.attributedText boundingRectWithSize:CGSizeMake(CGRectGetWidth(self.view.frame)-8.0f, 9999.0f) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:nil];
NSLog(@"%f", ceilf(textViewFrame.size.height));

Works on all fonts perfectly!

Sabatier answered 2/4, 2014 at 15:17 Comment(0)
I
1

I had the same problem, but I recognised that height constrained has been set correctly. So I did the following:

-(CGSize)MaxHeighForTextInRow:(NSString *)RowText width:(float)UITextviewWidth {

    CGSize constrainedSize = CGSizeMake(UITextviewWidth, CGFLOAT_MAX);

    NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                          [UIFont fontWithName:@"HelveticaNeue" size:11.0], NSFontAttributeName,
                                          nil];

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

    CGRect requiredHeight = [string boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin context:nil];

    if (requiredHeight.size.width > UITextviewWidth) {
        requiredHeight = CGRectMake(0, 0, UITextviewWidth, requiredHeight.size.height);
    }

    return requiredHeight.size;
}
Inherence answered 11/12, 2013 at 8:2 Comment(0)
E
1
    NSDictionary *stringAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
                                      [UIFont systemFontOfSize:18], NSFontAttributeName,
                                      [UIColor blackColor], NSForegroundColorAttributeName,
                                      nil];

    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:myLabel.text attributes:stringAttributes];
    myLabel.attributedText = attributedString; //this is the key!

    CGSize maximumLabelSize = CGSizeMake (screenRect.size.width - 40, CGFLOAT_MAX);

    CGRect newRect = [myLabel.text boundingRectWithSize:maximumLabelSize
                                                       options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
                                                    attributes:stringAttributes context:nil];

    self.myLabelHeightConstraint.constant = ceilf(newRect.size.height);

I tried everything on this page and still had one case for a UILabel that was not formatting correctly. Actually setting the attributedText on the label finally fixed the problem.

Encaenia answered 21/5, 2014 at 15:43 Comment(0)
C
1

One thing I was noticing is that the rect that would come back from (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)context would have a larger width than what I passed in. When this happened my string would be truncated. I resolved it like this:

NSString *aLongString = ...
NSInteger width = //some width;            
UIFont *font = //your font;
CGRect rect = [aLongString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
                                        options:(NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin)
                                     attributes:@{ NSFontAttributeName : font,
                                                   NSForegroundColorAttributeName : [UIColor whiteColor]}
                                        context:nil];

if(rect.size.width > width)
{
    return rect.size.height + font.lineHeight;
}
return rect.size.height;

For some more context; I had multi line text and I was trying to find the right height to display it in. boundRectWithSize was sometimes returning a width larger than what I would specify, thus when I used my past in width and the calculated height to display my text, it would truncate. From testing when boundingRectWithSize used the wrong width the amount it would make the height short by was 1 line. So I would check if the width was greater and if so add the font's lineHeight to provide enough space to avoid truncation.

Cuss answered 6/3, 2015 at 8:8 Comment(2)
How is the line height effecting with Width?Gardol
@RoiMulia line height doesn't affect the width. I updated my answer to provide more context as to how this fixed my bug.Cuss
F
1
Add Following methods in ur code for getting correct size of attribute string 
1.
    - (CGFloat)findHeightForText:(NSAttributedString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font
 {
    UITextView *textView = [[UITextView alloc] init];
    [textView setAttributedText:text];
    [textView setFont:font];
    CGSize size = [textView sizeThatFits:CGSizeMake(widthValue, FLT_MAX)];
    return size.height;

}

2. Call on heightForRowAtIndexPath method
     int h = [self findHeightForText:attrString havingWidth:yourScreenWidth andFont:urFont];
Fifteen answered 19/12, 2015 at 6:30 Comment(1)
In my case I should do the calculation on background thread, and usage of UI elements is not allowedWhoreson
F
1

After hours I've found out the the problem was in lineBreakMode. I had byTruncatingTail, but need .byWordWrapping (default).

enter image description here

Also when I was trying different ways to calculate height - found two another ways:

First

https://gist.github.com/krzyzanowskim/e92eaf31e0419820c0f8cbcf96ba1269#file-stringgetsizethatfits-swift

Second

func sizeFittingWidth(_ w: CGFloat) -> CGSize {
    let textStorage = NSTextStorage(attributedString: self)
    let size = CGSize(width: w, height: CGFloat.greatestFiniteMagnitude)
    let boundingRect = CGRect(origin: .zero, size: size)
    let textContainer = NSTextContainer(size: size)
    textContainer.lineFragmentPadding = 0
    let layoutManager = NSLayoutManager()
    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)
    layoutManager.glyphRange(forBoundingRect: boundingRect, in: textContainer)
    let rect = layoutManager.usedRect(for: textContainer)
    return rect.integral.size
}
Frisky answered 27/2, 2023 at 18:20 Comment(0)
D
0
    NSAttributedString *attributedText =[[[NSAttributedString alloc]
                                          initWithString:joyMeComment.content
                                          attributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:TextFont]}] autorelease];

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

    contentSize.size.height+=10;
    label.frame=contentSize;

if label's frame not add 10 this method will never work! hope this can help you! goog luck.

Divided answered 29/11, 2013 at 9:33 Comment(0)
T
0

I'd like to add my thoughts since I had exactly the same problem.

I was using UITextView since it had nicer text alignment (justify, which at the time was not available in UILabel), but in order to "simulate" non-interactive-non-scrollable UILabel, I'd switch off completely scrolling, bouncing, and user interaction.

Of course, problem was that text was dynamic, and while width would be fixed, height should be recalculated every time I'd set new text value.

boundingRectWithSize didn't work well for me at all, from what I could see, UITextView was adding some margin on top which boundingRectWithSize would not get into a count, hence, height retrieved from boundingRectWithSize was smaller than it should be.

Since text was not to be updated rapidly, it's just used for some information that may update every 2-3 seconds the most, I've decided following approach:

/* This f is nested in a custom UIView-inherited class that is built using xib file */
-(void) setTextAndAutoSize:(NSString*)text inTextView:(UITextView*)tv
{
    CGFloat msgWidth = tv.frame.size.width; // get target's width

    // Make "test" UITextView to calculate correct size
    UITextView *temp = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, msgWidth, 300)]; // we set some height, really doesn't matter, just put some value like this one.
    // Set all font and text related parameters to be exact as the ones in targeted text view
    [temp setFont:tv.font];
    [temp setTextAlignment:tv.textAlignment];
    [temp setTextColor:tv.textColor];
    [temp setText:text];

    // Ask for size that fits :P
    CGSize tv_size = [temp sizeThatFits:CGSizeMake(msgWidth, 300)];

    // kill this "test" UITextView, it's purpose is over
    [temp release];
    temp = nil;

    // apply calculated size. if calcualted width differs, I choose to ignore it anyway and use only height because I want to have width absolutely fixed to designed value
    tv.frame = CGRectMake(tv.frame.origin.x, tv.frame.origin.y, msgWidth, tv_size.height );
}

*Above code is not directly copied from my source, I had to adjust it / clear it from bunch of other stuff not needed for this article. Don't take it for copy-paste-and-it-will-work-code.

Obvious disadvantage is that it has alloc and release, for each call.

But, advantage is that you avoid depending on compatibility between how boundingRectWithSize draws text and calculates it's size and implementation of text drawing in UITextView (or UILabel which also you can use just replace UITextView with UILabel). Any "bugs" that Apple may have are this way avoided.

P.S. It would seem that you shouldn't need this "temp" UITextView and can just ask sizeThatFits directly from target, however that didn't work for me. Though logic would say it should work and alloc/release of temporary UITextView are not needed, it did not. But this solution worked flawlessly for any text I would set in.

Thresathresh answered 15/2, 2014 at 3:28 Comment(1)
In my case I should do the calculation on background thread, and usage of UI elements is not allowedWhoreson
P
0

Ok so I spent lots of time debugging this. I found out that the maximum text height as defined by boundingRectWithSize allowed to display text by my UITextView was lower than the frame size.

In my case the frame is at most 140pt but the UITextView tolerate texts at most 131pt.

I had to figure that out manually and hardcode the "real" maximum height.

Here is my solution:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    NSString *proposedText = [textView.text stringByReplacingCharactersInRange:range withString:text];
    NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:proposedText];
    CGRect boundingRect;
    CGFloat maxFontSize = 100;
    CGFloat minFontSize = 30;
    CGFloat fontSize = maxFontSize + 1;
    BOOL fit;
    NSLog(@"Trying text: \"%@\"", proposedText);
    do {
        fontSize -= 1;
        //XXX Seems like trailing whitespaces count for 0. find a workaround
        [attributedText addAttribute:NSFontAttributeName value:[textView.font fontWithSize:fontSize] range:NSMakeRange(0, attributedText.length)];
        CGFloat padding = textView.textContainer.lineFragmentPadding;
        CGSize boundingSize = CGSizeMake(textView.frame.size.width - padding * 2, CGFLOAT_MAX);
        boundingRect = [attributedText boundingRectWithSize:boundingSize options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading context:nil];
        NSLog(@"bounding rect for font %f is %@; (max is %f %f). Padding: %f", fontSize, NSStringFromCGRect(boundingRect), textView.frame.size.width, 148.0, padding);
        fit =  boundingRect.size.height <= 131;
    } while (!fit && fontSize > minFontSize);
    if (fit) {
        self.textView.font = [self.textView.font fontWithSize:fontSize];
        NSLog(@"Fit!");
    } else {
        NSLog(@"No fit");
    }
    return fit;
}
Peraea answered 29/11, 2017 at 3:44 Comment(0)
K
0

Encountered exactly same issue.

To me, the issue is solved by TTTAttributedLabel's

+ (CGSize)sizeThatFitsAttributedString:(NSAttributedString *)attributedString
                       withConstraints:(CGSize)size
                limitedToNumberOfLines:(NSUInteger)numberOfLines

method, as it provide accurate result.

Kilroy answered 10/8, 2020 at 9:17 Comment(0)
T
0

I had issues calculating the height of an NSTextField. Any method I tried was always returning values that were too small.
For me the problem turned out to be that, for some reason, NSTextField's attributedStringValue property never contained any of the attributes that I set via Interface Builder. It actually contained no attributes at all if I didn't set an attributed string programmatically. Not even a font. That is why all of the height calculations were botched.

To get it to work, I created a Category for NSTextField which implements a custom function for getting the correct attributed string.

Here's the implementation file for that Category:

//
// --------------------------------------------------------------------------
// NSTextField+Additions.m
// Created for Mac Mouse Fix (https://github.com/noah-nuebling/mac-mouse-fix)
// Created by Noah Nuebling in 2021
// Licensed under MIT
// --------------------------------------------------------------------------
//

#import "NSTextField+Additions.h"

@implementation NSTextField (Additions)

// Copy paste template for adding attributes to an attributed string. Contains all possible attributes

//    [str addAttributes:@{
//        NSFontAttributeName:                NSNull.null,
//        NSParagraphStyleAttributeName:      NSNull.null,
//        NSForegroundColorAttributeName:     NSNull.null,
//        NSBackgroundColorAttributeName:     NSNull.null,
//        NSLigatureAttributeName:            NSNull.null,
//        NSKernAttributeName:                NSNull.null,
//        NSStrikethroughStyleAttributeName:  NSNull.null,
//        NSUnderlineStyleAttributeName:      NSNull.null,
//        NSStrokeColorAttributeName:         NSNull.null,
//        NSStrokeWidthAttributeName:         NSNull.null,
//        NSShadowAttributeName:              NSNull.null,
//        NSTextEffectAttributeName:          NSNull.null,
//        NSAttachmentAttributeName:          NSNull.null,
//        NSLinkAttributeName:                NSNull.null,
//        NSBaselineOffsetAttributeName:      NSNull.null,
//        NSUnderlineColorAttributeName:      NSNull.null,
//        NSStrikethroughColorAttributeName:  NSNull.null,
//        NSObliquenessAttributeName:         NSNull.null,
//        NSExpansionAttributeName:           NSNull.null,
//        NSWritingDirectionAttributeName:    NSNull.null,
//        NSVerticalGlyphFormAttributeName:   NSNull.null,
//    } range:NSMakeRange(0, str.length)];

/// In my testing NSTextField.attributedStringValue actually returned a string without _any_ attributes. Not even a font or anything.
/// This lead to issues when trying to calculate the fitting height for a certain width of the NSTextField.
/// This function takes some of the properties of the NSTextField and returns an NSAttributed string based on those.
/// I'm not sure this is perfect, but the returned attributed string describes the way that the text of the NSTextField is rendered close enough to be usable for my height calculations
- (NSAttributedString *)effectiveAttributedStringValue {
    
    NSMutableAttributedString *str = self.attributedStringValue.mutableCopy;

    // Create paragraph style from NSTextField properties
    
    // Not sure if we're setting these properties correctly, and there could be more properties we should be setting
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.alignment = self.alignment;
    paragraphStyle.baseWritingDirection = self.baseWritingDirection;
    paragraphStyle.lineBreakMode = self.lineBreakMode;
    paragraphStyle.allowsDefaultTighteningForTruncation = self.allowsDefaultTighteningForTruncation;
    if (@available(macOS 10.15, *)) paragraphStyle.lineBreakStrategy = self.lineBreakStrategy;
    
    // Add attributes to AttributedString based on NSTextField properties
     
    [str addAttributes:@{
        NSFontAttributeName:                self.font,
        NSParagraphStyleAttributeName:      paragraphStyle,
        NSForegroundColorAttributeName:     self.textColor,
        NSBackgroundColorAttributeName:     self.backgroundColor,
//        NSLigatureAttributeName:            NSNull.null,
//        NSKernAttributeName:                NSNull.null,
//        NSStrikethroughStyleAttributeName:  NSNull.null,
//        NSUnderlineStyleAttributeName:      NSNull.null,
//        NSStrokeColorAttributeName:         NSNull.null,
//        NSStrokeWidthAttributeName:         NSNull.null,
//        NSShadowAttributeName:              NSNull.null, //self.shadow,
//        NSTextEffectAttributeName:          NSNull.null,
//        NSAttachmentAttributeName:          NSNull.null,
//        NSLinkAttributeName:                NSNull.null,
//        NSBaselineOffsetAttributeName:      NSNull.null, //self.baselineOffsetFromBottom,
//        NSUnderlineColorAttributeName:      NSNull.null,
//        NSStrikethroughColorAttributeName:  NSNull.null,
//        NSObliquenessAttributeName:         NSNull.null,
//        NSExpansionAttributeName:           NSNull.null,
//        NSWritingDirectionAttributeName:    NSNull.null, //self.baseWritingDirection,
//        NSVerticalGlyphFormAttributeName:   NSNull.null,
    } range:NSMakeRange(0, str.length)];
    
    // return NSAttributedString
    
    return str;
    
}

@end


Random Sidenotes

  • Some of the issues I've read about people having with UILabel in this thread sound a lot like they might be related.
  • I eventually decided to use NSTextView over NSTextField because its methods for obtaining the attributed string work out of the box, and using NSTextField for clickable links was completely botched as well. I'm under the impression that NSTextField is just a buggy mess that you should avoid beyond the most basic of use-cases.
Trophy answered 20/3, 2021 at 14:29 Comment(0)
F
0

I was having issues sometimes calculating some heights with boundingRect, specially with paragraphs and break lines. Adding .usesDeviceMetrics as a parameter did the trick. Now seems to work fine in all cases.

extension NSAttributedString {

    func heightWithWidth(_ width: CGFloat) -> CGFloat {

        let constraints = CGSize(width: width, height: .infinity)

        let bounding = self.boundingRect(with: constraints, options: [.usesLineFragmentOrigin, .usesFontLeading, .usesDeviceMetrics], context: nil)
        return bounding.height

    }
}
   
Foretopmast answered 3/6, 2022 at 13:48 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.