221

I am trying to calculate the height of a UILabel based on different String lengths.

func calculateContentHeight() -> CGFloat{
    var maxLabelSize: CGSize = CGSizeMake(frame.size.width - 48, CGFloat(9999))
    var contentNSString = contentText as NSString
    var expectedLabelSize = contentNSString.boundingRectWithSize(maxLabelSize, options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: [NSFontAttributeName: UIFont.systemFontOfSize(16.0)], context: nil)
    print("\(expectedLabelSize)")
    return expectedLabelSize.size.height

}

Above is the current function I use to determine the height but it is not working. I would greatly appreciate any help I can get. I would perfer the answer in Swift and not Objective C.

Adam Eberbach
  • 12,309
  • 6
  • 62
  • 114
Cody Weaver
  • 4,756
  • 11
  • 33
  • 51

14 Answers14

619

Use an extension on String

Swift 3

extension String {
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
    
        return ceil(boundingBox.height)
    }

    func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return ceil(boundingBox.width)
    }
}

and also on NSAttributedString (which is very useful at times)

extension NSAttributedString {
    func height(withConstrainedWidth width: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
    
        return ceil(boundingBox.height)
    }

    func width(withConstrainedHeight height: CGFloat) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
    
        return ceil(boundingBox.width)
    }
}

Swift 4 & 5

Just change the value for attributes in the extension String methods

from

[NSFontAttributeName: font]

to

[.font : font]
Kaan Dedeoglu
  • 14,765
  • 5
  • 40
  • 41
  • if I was using the extension on `String` but wanted to get the width instead of the height. I have tried using the exact same code except all places you used height I switched to use width and it doesn't work. – Cody Weaver Oct 27 '15 at 05:57
  • 2
    @CodyWeaver check the edit for widthWithConstrainedHeight method. – Kaan Dedeoglu Oct 27 '15 at 12:12
  • 9
    @KaanDedeoglu how would this work with dynamic height strings like when you use "numberOfLines" = 0 (which might be UILabel specific, not sure) or lineBreakMode ByWordWrapping. My guess was to add that to the attributes like this `[NSFontAttributeName: font, NSLineBreakMode: .ByWordWrapping]` but it didn't work – Francisc0 Apr 13 '16 at 20:50
  • 4
    Think I figured out my answer. I need to use `NSParagraphStyleAttributeName : style` where style is NSMutableParagraphStyle – Francisc0 Apr 13 '16 at 21:06
  • Your extension on String allows me to paginate my variable length paragraphs written to a PDF context. Thanks. – Magnas Jan 31 '17 at 09:44
  • 1
    One thing with this answer I've found when using it in `sizeForItemAtIndexPath` in a `UICollectionView` is that it seems to override the return for `insetForSectionAt` – Zack Shapiro Apr 04 '17 at 20:04
  • If anyone wants a reference of the iOS font names and additional details have a look at this handy website: http://iosfonts.com – Sharukh Mastan Apr 08 '17 at 10:17
  • 1
    Hi had to recover the Swift2 version from your history! You could include that in your answer, for others like me! ;) – Nem Jun 18 '17 at 08:40
  • Just a tip.. you must round up using the `ceil` function because sizes are returned as fractional values and it specifies that you must use the `ceil` function in the docs: https://developer.apple.com/documentation/foundation/nsattributedstring/1529154-boundingrect – Harish Jul 28 '17 at 02:26
  • Thank you for the answer. A problem, perhaps due to my understand of how boundingRect(:) works... if I call "height(withConstrainedWidth:" on the string "a", it gives me the same height as for the string "A". What am I missing? According to the docs, it should return a bounding rect according to each glyph in the string. – Womble Mar 06 '18 at 03:37
  • if have issue with 'NSFontAttributeName' look here https://stackoverflow.com/questions/50364183/use-of-unresolved-identifier-nsfontattributename – Muhammad Asyraf Jun 08 '18 at 08:39
  • 1
    I found that the accepted answer worked for a fixed width, but not a fixed height. For a fixed height, it would just increase the width to fit everything on one line, unless there was a line break in the text. Here is my alternate answer: [My Answer](https://stackoverflow.com/a/54114620/5372480) – MkSMC Jan 09 '19 at 16:37
  • It didn't worked for my attributed string, getting wrong height – Arshad Shaik Nov 24 '21 at 04:46
21

For multiline text this answer is not working correctly. You can build a different String extension by using UILabel

extension String {
func height(constraintedWidth width: CGFloat, font: UIFont) -> CGFloat {
    let label =  UILabel(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.text = self
    label.font = font
    label.sizeToFit()

    return label.frame.height
 }
}

The UILabel gets a fixed width and the .numberOfLines is set to 0. By adding the text and calling .sizeToFit() it automatically adjusts to the correct height.

Code is written in Swift 3

Sn0wfreeze
  • 1,959
  • 3
  • 18
  • 32
  • 20
    sizeToFit however introduces a million of perfomance issues due to the many passes of the drawing. Calculating the size manually is way cheaper in resources – Marco Pappalardo Nov 18 '16 at 17:00
  • 2
    This solution should set the UIFont of the UILabel to ensure the correct height. – Matthew Spencer Aug 21 '17 at 04:00
  • works perfectly for me - even accounts for empty strings (unlike the accepted answer). Very handy for calculating the height of a tableView with automatic height cells! – Hendies Apr 01 '18 at 17:21
  • I found that the accepted answer worked for a fixed width, but not a fixed height. For a fixed height, it would just increase the width to fit everything on one line, unless there was a line break in the text. Here is my alternate answer: [My Answer](https://stackoverflow.com/a/54114620/5372480) – MkSMC Jan 09 '19 at 16:39
  • 2
    I posted a similar solution-- without the need for a call to `sizeToFit` – RyanG Jul 01 '19 at 19:51
  • Does this solution works for ANY available font on iOS? I have some serious issues when for some specific font returned height is too big (1 empty line). – sabiland Aug 02 '19 at 05:23
21

Heres a simple solution thats working for me... similar to some of the others posted, but it doesn't need the sizeToFit

Note this is written in Swift 5

let lbl = UILabel()
lbl.numberOfLines = 0
lbl.font = UIFont.systemFont(ofSize: 12) // make sure you set this correctly 
lbl.text = "My text that may or may not wrap lines..."

let width = 100.0 // the width of the view you are constraint to, keep in mind any applied margins here
 
let height = lbl.systemLayoutSizeFitting(CGSize(width: width, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel).height

This handles line wrapping and such. Not the most elegant code, but it gets the job done.

RyanG
  • 4,393
  • 2
  • 39
  • 64
7

This is my answer in Swift 4.1 and Xcode 9.4.1

//This is your label
let proNameLbl = UILabel(frame: CGRect(x: 0, y: 20, width: 300, height: height))
proNameLbl.text = "This is your text"
proNameLbl.font = UIFont.systemFont(ofSize: 17)
proNameLbl.numberOfLines = 0
proNameLbl.lineBreakMode = .byWordWrapping
infoView.addSubview(proNameLbl)

//Function to calculate height for label based on text
func heightForView(text:String, font:UIFont, width:CGFloat) -> CGFloat {
    let label:UILabel = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
    label.numberOfLines = 0
    label.lineBreakMode = NSLineBreakMode.byWordWrapping
    label.font = font
    label.text = text

    label.sizeToFit()
    return label.frame.height
}

Now you call this function

//Call this function
let height = heightForView(text: "This is your text", font: UIFont.systemFont(ofSize: 17), width: 300)
print(height)//Output : 41.0
Naresh
  • 16,698
  • 6
  • 112
  • 113
6

Swift 5:

If you have UILabel and someway boundingRect isn't working for you (I faced this problem. It always returned 1 line height.) there is an extension to easily calculate label size.

extension UILabel {
    func getSize(constrainedWidth: CGFloat) -> CGSize {
        return systemLayoutSizeFitting(CGSize(width: constrainedWidth, height: UIView.layoutFittingCompressedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    }
}

You can use it like this:

let label = UILabel()
label.text = "My text\nIs\nAwesome"
let labelSize = label.getSize(constrainedWidth:200.0)

Works for me

George Sabanov
  • 179
  • 1
  • 8
4
extension String{

    func widthWithConstrainedHeight(_ height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: CGFloat.greatestFiniteMagnitude, height: height)

        let boundingBox = self.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return ceil(boundingBox.width)
    }

    func heightWithConstrainedWidth(_ width: CGFloat, font: UIFont) -> CGFloat? {
        let constraintRect = CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)

        return ceil(boundingBox.height)
    }

}
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
CrazyPro007
  • 1,006
  • 9
  • 15
2

I found that the accepted answer worked for a fixed width, but not a fixed height. For a fixed height, it would just increase the width to fit everything on one line, unless there was a line break in the text.

The width function calls the height function multiple times, but it is a quick calculation and I didn't notice performance issues using the function in the rows of a UITable.

extension String {

    public func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font : font], context: nil)

        return ceil(boundingBox.height)
    }

    public func width(withConstrainedHeight height: CGFloat, font: UIFont, minimumTextWrapWidth:CGFloat) -> CGFloat {

        var textWidth:CGFloat = minimumTextWrapWidth
        let incrementWidth:CGFloat = minimumTextWrapWidth * 0.1
        var textHeight:CGFloat = self.height(withConstrainedWidth: textWidth, font: font)

        //Increase width by 10% of minimumTextWrapWidth until minimum width found that makes the text fit within the specified height
        while textHeight > height {
            textWidth += incrementWidth
            textHeight = self.height(withConstrainedWidth: textWidth, font: font)
        }
        return ceil(textWidth)
    }
}
MkSMC
  • 101
  • 1
  • 8
  • 1
    what is `minimumTextWrapWidth:CGFloat`? – Vyachaslav Gerchicov Feb 20 '19 at 09:32
  • It is just a seed value for the calculations in the function. If you expect the width to be large, picking a small minimumTextWrapWidth will cause the while loop to go through additional iterations. So the larger the minimum width the better, but if it is larger than the actual width required, then it will always be the width returned. – MkSMC May 28 '19 at 16:25
1

In Swift 5:

label.textRect(forBounds: label.bounds, limitedToNumberOfLines: 1)

btw, the value of limitedToNumberOfLines depends on your label's text lines you want.

gcharita
  • 7,729
  • 3
  • 20
  • 37
varrtix
  • 61
  • 4
0
@IBOutlet weak var constraintTxtV: NSLayoutConstraint!
func TextViewDynamicallyIncreaseSize() {
    let contentSize = self.txtVDetails.sizeThatFits(self.txtVDetails.bounds.size)
    let higntcons = contentSize.height
    constraintTxtV.constant = higntcons
}
MechMK1
  • 3,278
  • 7
  • 37
  • 55
Niraj Paul
  • 1,498
  • 14
  • 22
  • 5
    Your answer should not only consist of code, but also of an explanation regarding the code. Please see [answer] for more details. – MechMK1 Nov 24 '17 at 10:11
  • While this code may answer the question, providing additional context regarding why and/or how this code answers the question improves its long-term value. – Isma Nov 24 '17 at 13:25
  • This answer is incomplete. It refers to important variables who's types are unknown which defeats the purpose – bitwit May 08 '18 at 21:54
0

Check label text height and it is working on it

let labelTextSize = ((labelDescription.text)! as NSString).boundingRect(
                with: CGSize(width: labelDescription.frame.width, height: .greatestFiniteMagnitude),
                options: .usesLineFragmentOrigin,
                attributes: [.font: labelDescription.font],
                context: nil).size
            if labelTextSize.height > labelDescription.bounds.height {
                viewMoreOrLess.hide(byHeight: false)
                viewLess.hide(byHeight: false)
            }
            else {
                viewMoreOrLess.hide(byHeight: true)
                viewLess.hide(byHeight: true)

            }
Srinivasan_iOS
  • 972
  • 10
  • 12
0

This solution will help to calculate the height and width at runtime.

    let messageText = "Your Text String"
    let size = CGSize.init(width: 250, height: 1000)
    let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
    let estimateFrame = NSString(string: messageText).boundingRect(with:  size, options: options, attributes: [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue", size: 17)!], context: nil)

Here you can calculate the estimated height that your string would take and pass it to the UILabel frame.

estimateFrame.Width
estimateFrame.Height 
Shanu Singh
  • 365
  • 3
  • 16
0

I was not able to get @KaanDedeoglu's solution to work in Swift 5 for multiline labels and text views - for whatever reason - so I just ended up writing a 'by hand' solution keeping the same function signatures as seen in @KaanDedeoglu's answer for those who are interested. Works like a charm for the uses in my program.

Width

extension String {

    func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
        
        var wordBoxes = [CGSize]()
        var calculatedHeight = CGFloat.zero
        var calculatedWidth = CGFloat.zero
        
        for word in self.wordsWithWordSeparators() {
            
            let box = word.boundingRect(with: CGSize.zero, attributes: [.font: font], context: nil)
            let boxSize = CGSize(width: box.width, height: box.height)
            wordBoxes.append(boxSize)
            calculatedHeight += boxSize.height
            calculatedWidth = calculatedWidth < boxSize.width ? boxSize.width : calculatedWidth
        }
        
        while calculatedHeight > height && wordBoxes.count > 1 {
            
            var bestLineToRelocate = wordBoxes.count - 1
            
            for i in 1..<wordBoxes.count {
                
                let bestPotentialWidth = wordBoxes[bestLineToRelocate - 1].width + wordBoxes[bestLineToRelocate].width
                let thisPotentialWidth = wordBoxes[i - 1].width + wordBoxes[i].width
                
                if bestPotentialWidth > thisPotentialWidth {
                    bestLineToRelocate = i
                }
            }
            
            calculatedHeight -= wordBoxes[bestLineToRelocate].height
            wordBoxes[bestLineToRelocate - 1].width += wordBoxes[bestLineToRelocate].width
            wordBoxes.remove(at: bestLineToRelocate)
            calculatedWidth = max(wordBoxes[bestLineToRelocate - 1].width, calculatedWidth)
        }
        
        return ceil(calculatedWidth)
    }
}

Height

extension String {
    
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        
        var wordBoxes = [CGSize]()
        var calculatedHeight = CGFloat.zero
        var currentLine = 0
        
        for word in self.wordsWithWordSeparators() {
            
            let box = word.boundingRect(with: CGSize.zero, attributes: [.font: font], context: nil)
            let boxSize = CGSize(width: box.width, height: box.height)
            
            if wordBoxes.isEmpty == true {
                wordBoxes.append(boxSize)
            }
            else if wordBoxes[currentLine].width + boxSize.width > width {
                wordBoxes.append(boxSize)
                currentLine += 1
            }
            else {
                wordBoxes[currentLine].width += boxSize.width
                wordBoxes[currentLine].height = max(wordBoxes[currentLine].height, boxSize.height)
            }
        }
        
        for wordBox in wordBoxes {
            calculatedHeight += wordBox.height
        }
        
        return calculatedHeight
    }
}

Helper Methods Used

extension String {
    
    // Should work with any language supported by Apple
    func wordsWithWordSeparators () -> [String] {
        
        let range = self.startIndex..<self.endIndex
        var words = [String]()
        
        self.enumerateSubstrings(in: range, options: .byWords) { (substr, substrRange, enclosingRange, stop) in
            let wordWithWordSeparators = String(self[enclosingRange])
            words.append(wordWithWordSeparators)
        }
        
        return words
    }
}

Note: these height and width calculations assume the given label or text view will not split or hyphenate words when performing line breaks. If this is not the case for you, you should only have to substitute words for characters. Also, if you're in a runtime sensitive environment, may want to consider throttling these function calls or caching results since they could be a bit expensive depending on how many words the string contains.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Garret Kaye
  • 2,412
  • 4
  • 21
  • 45
0

I created the following extension to calculate height of the String based on the width.

   extension String {
    func heightWithConstrainedWidth(width: CGFloat, font: UIFont) -> CGFloat {
        let textView = UITextView()
        textView.text = self
        textView.font = font
        var newFrame = textView.frame
        newFrame.size = CGSize(width: width, height: .greatestFiniteMagnitude)
        textView.frame = newFrame
        return textView.contentSize.height
    }
0
extension String {
    func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
        
        return ceil(boundingBox.height)
    }
    
    func width(withConstraintedHeight height: CGFloat, font: UIFont) -> CGFloat {
        let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
        let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
        
        return ceil(boundingBox.width)
    }
}