How to get height for NSAttributedString at a fixed width

CocoaNstextviewAppkitNsattributedstring

Cocoa Problem Overview


I want to do some drawing of NSAttributedStrings in fixed-width boxes, but am having trouble calculating the right height they'll take up when drawn. So far, I've tried:

  1. Calling - (NSSize) size, but the results are useless (for this purpose), as they'll give whatever width the string desires.

  2. Calling - (void)drawWithRect:(NSRect)rect options:(NSStringDrawingOptions)options with a rect shaped to the width I want and NSStringDrawingUsesLineFragmentOrigin in the options, exactly as I'm using in my drawing. The results are ... difficult to understand; certainly not what I'm looking for. (As is pointed out in a number of places, including http://lists.apple.com/archives/cocoa-dev/2006/Apr/msg01540.html">this</a> Cocoa-Dev thread).

  3. Creating a temporary NSTextView and doing:
    [[tmpView textStorage] setAttributedString:aString];
    [tmpView setHorizontallyResizable:NO];
    [tmpView sizeToFit];

    When I query the frame of tmpView, the width is still as desired, and the height is often correct ... until I get to longer strings, when it's often half the size that's required. (There doesn't seem to be a max size being hit: one frame will be 273.0 high (about 300 too short), the other will be 478.0 (only 60-ish too short)).

I'd appreciate any pointers, if anyone else has managed this.

Cocoa Solutions


Solution 1 - Cocoa

-[NSAttributedString boundingRectWithSize:options:]

You can specify NSStringDrawingUsesDeviceMetrics to get [union of all glyph bounds][1].

Unlike -[NSAttributedString size], the returned NSRect represents the dimensions of the area that would change if the string is drawn.

As @Bryan comments, boundingRectWithSize:options: is deprecated (not recommended) in OS X 10.11 and later. This is because string styling is now dynamic depending on the context.

For OS X 10.11 and later, see Apple's Calculating Text Height developer documentation.

[1]: http://lists.apple.com/archives/Cocoa-dev/2006/Feb/msg00564.html "Re: Getting exact height of NSAttributedString"

Solution 2 - Cocoa

The answer is to use
- (void)drawWithRect:(NSRect)rect options:(NSStringDrawingOptions)options
but the rect you pass in should have 0.0 in the dimension you want to be unlimited (which, er, makes perfect sense). Example http://teilweise.tumblr.com/post/293948288/boundingrectwithsize-options-attributes">here</a>;.

Solution 3 - Cocoa

I have a complex attributed string with multiple fonts and got incorrect results with a few of the above answers that I tried first. Using a UITextView gave me the correct height, but was too slow for my use case (sizing collection cells). I wrote swift code using the same general approach described in the Apple doc referenced previously and described by Erik. This gave me correct results with must faster execution than having a UITextView do the calculation.

private func heightForString(_ str : NSAttributedString, width : CGFloat) -> CGFloat {
    let ts = NSTextStorage(attributedString: str)
    
    let size = CGSize(width:width, height:CGFloat.greatestFiniteMagnitude)
    
    let tc = NSTextContainer(size: size)
    tc.lineFragmentPadding = 0.0
    
    let lm = NSLayoutManager()
    lm.addTextContainer(tc)
    
    ts.addLayoutManager(lm)
    lm.glyphRange(forBoundingRect: CGRect(origin: .zero, size: size), in: tc)
    
    let rect = lm.usedRect(for: tc)

    return rect.integral.size.height
}

Solution 4 - Cocoa

You might be interested in Jerry Krinock's great (OS X only) NS(Attributed)String+Geometrics category, which is designed to do all sorts of string measurement, including what you're looking for.

Solution 5 - Cocoa

On OS X 10.11+, the following method works for me (from Apple's Calculating Text Height document)

- (CGFloat)heightForString:(NSAttributedString *)myString atWidth:(float)myWidth
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:myString];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:
        NSMakeSize(myWidth, FLT_MAX)];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];
    [layoutManager glyphRangeForTextContainer:textContainer];
    return [layoutManager
        usedRectForTextContainer:textContainer].size.height;
}

Solution 6 - Cocoa

Swift 4.2

let attributedString = self.textView.attributedText
let rect = attributedString?.boundingRect(with: CGSize(width: self.textView.frame.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
print("attributedString Height = ",rect?.height)

Solution 7 - Cocoa

Use NSAttributedString method

- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(NSStringDrawingContext *)context

The size is the constraint on the area, the calculated area width is restricted to the specified width whereas the height is flexible based on this width. One can specify nil for context if that's not available. To get multi-line text size, use NSStringDrawingUsesLineFragmentOrigin for options.

Solution 8 - Cocoa

I just wasted a bunch of time on this, so I'm providing an additional answer to save others in the future. Graham's answer is 90% correct, but it's missing one key piece:

To obtain accurate results with -boundingRectWithSize:options: you MUST pass the following options:

NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesDeviceMetrics|NSStringDrawingUsesFontLeading

If you omit the lineFragmentOrigin one, you'll get nonsense back; the returned rect will be a single line high and won't at all respect the size you pass into the method.

Why this is so complicated and so poorly documented is beyond me. But there you have it. Pass those options and it'll work perfectly (on OS X at least).

Solution 9 - Cocoa

Swift 3:

let attributedStringToMeasure = NSAttributedString(string: textView.text, attributes: [
        NSFontAttributeName: UIFont(name: "GothamPro-Light", size: 15)!,
        NSForegroundColorAttributeName: ClickUpConstants.defaultBlackColor
])

let placeholderTextView = UITextView(frame: CGRect(x: 0, y: 0, width: widthOfActualTextView, height: 10))
placeholderTextView.attributedText = attributedStringToMeasure
let size: CGSize = placeholderTextView.sizeThatFits(CGSize(width: widthOfActualTextView, height: CGFloat.greatestFiniteMagnitude))

height = size.height

This answer works great for me, unlike the other ones which were giving me incorrect heights for larger strings.

If you want to do this with regular text instead of attributed text, do the following:

let placeholderTextView = UITextView(frame: CGRect(x: 0, y: 0, width: ClickUpConstants.screenWidth - 30.0, height: 10))
placeholderTextView.text = "Some text"
let size: CGSize = placeholderTextView.sizeThatFits(CGSize(width: widthOfActualTextView, height: CGFloat.greatestFiniteMagnitude))

height = size.height

Solution 10 - Cocoa

As lots of guys mentioned above, and base on my test.

I use open func boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], context: NSStringDrawingContext?) -> CGRect on iOS like this bellow:

let rect = attributedTitle.boundingRect(with: CGSize(width:200, height:0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)

Here the 200 is the fixed width as your expected, height I give it 0 since I think it's better to kind tell API height is unlimited.

Option is not so important here,I have try .usesLineFragmentOrigin or .usesLineFragmentOrigin.union(.usesFontLeading) or .usesLineFragmentOrigin.union(.usesFontLeading).union(.usesDeviceMetrics), it give same result.

And the result is expected as my though.

Thanks.

Solution 11 - Cocoa

Not a single answer on this page worked for me, nor did that ancient old Objective-C code from Apple's documentation. What I finally did get to work for a UITextView is first setting its text or attributedText property on it and then calculating the size needed like this:

let size = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.max))

Works perfectly. Booyah!

Solution 12 - Cocoa

I found helper class to find height and width of attributedText (Tested code)

https://gist.github.com/azimin/aa1a79aefa1cec031152fa63401d2292

Add above file in your project

How to use

let attribString = AZTextFrameAttributes(attributedString: lbl.attributedText!)
let width : CGFloat = attribString.calculatedTextWidth()
print("width is :: >> \(width)")

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbonaldiView Question on Stackoverflow
Solution 1 - CocoaGraham MilnView Answer on Stackoverflow
Solution 2 - CocoabonaldiView Answer on Stackoverflow
Solution 3 - CocoaMatt RView Answer on Stackoverflow
Solution 4 - CocoaRob KenigerView Answer on Stackoverflow
Solution 5 - CocoaErikView Answer on Stackoverflow
Solution 6 - CocoaZAFAR007View Answer on Stackoverflow
Solution 7 - CocoaCodeBrewView Answer on Stackoverflow
Solution 8 - CocoaBryanView Answer on Stackoverflow
Solution 9 - CocoaJosh O'ConnorView Answer on Stackoverflow
Solution 10 - CocoaJerryZhouView Answer on Stackoverflow
Solution 11 - CocoaPatrick LynchView Answer on Stackoverflow
Solution 12 - CocoaHardik ThakkarView Answer on Stackoverflow