52

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 this 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.

bonaldi
  • 992
  • 1
  • 8
  • 11

12 Answers12

31
-[NSAttributedString boundingRectWithSize:options:]

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

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.

Graham Miln
  • 2,724
  • 3
  • 33
  • 33
  • It seems that if you use `NSStringDrawingUsesDeviceMetrics` and there are no glyphs that this will return `0`. Is there a way to handle both situations? Or must you some how know or keep track of whether you added a glyph? (Tested on iOS 8.) – zekel Oct 28 '14 at 21:28
  • It makes sense that a string with no visible content would return a zero sized rect. Are you seeing new behaviour on iOS 8 compared to iOS 7? – Graham Miln Oct 29 '14 at 05:22
  • I meant that if you have an `NSAttributedString` with text (and no glyphs) and you use `NSStringDrawingUsesDeviceMetrics` the size is `CGSizeZero`. So it seems like you have to know if there are glyphs before you decide how to get the size... – zekel Nov 01 '14 at 08:50
  • Is it possible to have text without [glyphs](http://en.wikipedia.org/wiki/Glyph)? Is this a common situation? – Graham Miln Nov 01 '14 at 10:07
  • Ah, terminology issue. By glyphs I meant `NSTextAttachments` images in the `NSAttributedString.` But from your response I gather that `NSStringDrawingUsesDeviceMetrics` should be able to be used regardless of NSTextAttachments? – zekel Nov 01 '14 at 23:22
  • Yes, assuming you want the [bounds of the affected pixels](https://developer.apple.com/library/ios/documentation/uikit/reference/NSAttributedString_UIKit_Additions/index.html#//apple_ref/c/tdef/NSStringDrawingOptions). The best way is to create a small test app and confirm the behaviour matches your needs. – Graham Miln Nov 02 '14 at 10:53
  • NOTE: You must pass the NSStringDrawingUsesLineFragmentOrigin option for this approach to return non-nonsense results. See my answer below for more information. – Bryan Sep 15 '15 at 02:17
  • 3
    Bad news: this method is deprecated starting on OS 10.11 AND it no longer returns the correct height. It's too low. Works great on 10.10 and below, though. – Bryan Oct 17 '15 at 04:32
  • This is outdated. See https://stackoverflow.com/a/54980862/326242 for the up-to-date answer. – Andrew Koster Jan 25 '21 at 20:40
14

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 here.

bonaldi
  • 992
  • 1
  • 8
  • 11
12

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
}
Matt R
  • 804
  • 1
  • 9
  • 8
  • This solved my issue. I have an attributed string with a lot of attributes all with different ranges and the typical measurement APIs weren't quite working. Thanks for this answer. – naturaln0va Jun 10 '20 at 23:40
  • This is outdated. See https://stackoverflow.com/a/54980862/326242 for the up-to-date answer. – Andrew Koster Jan 25 '21 at 20:40
5

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.

zekel
  • 9,227
  • 10
  • 65
  • 96
Rob Keniger
  • 45,830
  • 6
  • 101
  • 134
  • 1
    That code appears to be Mac specific. It won't help with iOS development. – Brennan Aug 23 '13 at 22:36
  • 3
    You're right, the question was about `NSTextView`, which is a Mac class. I'm not even sure iOS had `NSAttributedString` when I answered this question. – Rob Keniger Aug 24 '13 at 02:46
  • 1
    Late input, but just a heads up: I've had problems with NS(Attributed)String+Geometrics consistently underestimating the height that long strings require. Until now, I've actually just fudged the result by adding an extra 25%. I'm now looking for a newer, better solution. I do not think NS(Attributed)String+Geometrics is working well on 10.9, 10.10 and 10.11. – Bryan Sep 15 '15 at 01:46
  • 1
    This is outdated. See https://stackoverflow.com/a/54980862/326242 for the up-to-date answer. – Andrew Koster Jan 25 '21 at 20:41
3

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;
}
Erik
  • 2,138
  • 18
  • 18
  • This is outdated. See https://stackoverflow.com/a/54980862/326242 for the up-to-date answer. – Andrew Koster Jan 25 '21 at 20:41
  • This is the only thing that worked for me, after tons of trouble with `boundingRectWithSize:` thanks! – Noah Nuebling Apr 15 '21 at 17:29
  • @AndrewKoster you should consider deleting your comment(s). It has confused me and deterred me from trying a solution that ended up working for me over the one you say is the up-to-date answer. – Noah Nuebling Apr 15 '21 at 17:31
  • ... However this only seemed to work when I provided a value other than FLT_MAX for the width. – Noah Nuebling Apr 15 '21 at 17:42
  • @NoahNuebling My link is pointing to the answer by @ZAFAR007. Stack Overflow's CSS is slightly off, which is unfortunate. @ZAFAR007's answer is in Swift and uses `boundingRect`. It is both simpler than this answer and, from my tests, worked where this answer did not. – Andrew Koster Apr 15 '21 at 23:02
  • @AndrewKoster I was using attributed strings with bold text and links (including underlines). The `boundingRect` method seemed to work pretty well but broke in some edge cases. It slightly underestimated the width (IIRC). I tried lots of things to make it work but it just didn't for me. A comment above even mentions that the `boundingRect` method is deprecated in 10.11+. In contrast, the `layourManager` solution is straight from Apple docs, which aren't deprecated as far as I can tell. So I think this solution has a higher chance of working for most people. – Noah Nuebling Apr 16 '21 at 15:54
3

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
Josh O'Connor
  • 4,694
  • 7
  • 54
  • 98
3

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)
ZAFAR007
  • 3,049
  • 1
  • 34
  • 45
2

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).

Alessandro Vendruscolo
  • 14,493
  • 4
  • 32
  • 41
Bryan
  • 4,628
  • 3
  • 36
  • 62
  • Also: this assumes you've got an NSAttributedString created with a given font and the NSParagraphStyle you're looking for -- in my case, that's a left-aligned, word-wrapping, no-hyphenation paragraph style. Don't forget to create that when you create your attributedString. – Bryan Sep 15 '15 at 02:19
  • This method is deprecated on 10.11+ and no longer works correctly; the returned height is too small on El Capitan. – Bryan Oct 17 '15 at 04:33
1

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.

CodeBrew
  • 6,457
  • 2
  • 43
  • 48
1

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!

Patrick Lynch
  • 2,742
  • 1
  • 16
  • 18
1

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.

JerryZhou
  • 4,566
  • 3
  • 37
  • 60
0

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)")
Hardik Thakkar
  • 15,269
  • 2
  • 94
  • 81
  • That helper class does the calculation twice (height and width). Calculating string size is not cheap so it's an unnecessary overhead if you're doing a lot of layout. – Andy Dent Jul 01 '20 at 10:11