Figure out size of UILabel based on String in Swift
Asked Answered
C

14

226

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.

Columba answered 26/5, 2015 at 5:50 Comment(1)
duplicate try this https://mcmap.net/q/120169/-how-to-get-the-height-of-a-uilabel-in-swiftBourgeois
L
624

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]
Lisa answered 26/5, 2015 at 5:59 Comment(13)
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.Columba
@CodyWeaver check the edit for widthWithConstrainedHeight method.Lisa
@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 workInsessorial
Think I figured out my answer. I need to use NSParagraphStyleAttributeName : style where style is NSMutableParagraphStyleInsessorial
Your extension on String allows me to paginate my variable length paragraphs written to a PDF context. Thanks.Bathhouse
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 insetForSectionAtAnimalize
If anyone wants a reference of the iOS font names and additional details have a look at this handy website: iosfonts.comSheba
Hi had to recover the Swift2 version from your history! You could include that in your answer, for others like me! ;)Hemihedral
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: developer.apple.com/documentation/foundation/nsattributedstring/…Forrester
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.Hanshaw
if have issue with 'NSFontAttributeName' look here #50364683Mchugh
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 AnswerWhitcher
It didn't worked for my attributed string, getting wrong heightSoler
W
24

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.

Wiese answered 1/7, 2019 at 19:51 Comment(1)
Great answer ! ThanksCarducci
C
22

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 🔶🐦

Centro answered 17/11, 2016 at 10:41 Comment(6)
sizeToFit however introduces a million of perfomance issues due to the many passes of the drawing. Calculating the size manually is way cheaper in resourcesTearoom
This solution should set the UIFont of the UILabel to ensure the correct height.Potomac
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!Tribute
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 AnswerWhitcher
I posted a similar solution-- without the need for a call to sizeToFitWiese
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).Phenolic
S
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
Sec answered 16/8, 2018 at 12:33 Comment(0)
M
7

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

Mangrove answered 21/5, 2020 at 11:27 Comment(0)
P
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)
    }

}
Pirali answered 14/5, 2018 at 12:21 Comment(0)
W
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)
    }
}
Whitcher answered 9/1, 2019 at 16:35 Comment(2)
what is minimumTextWrapWidth:CGFloat?Spraddle
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.Whitcher
B
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.

Blowzed answered 9/11, 2020 at 1:0 Comment(0)
A
1

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.

Aeniah answered 2/11, 2021 at 3:6 Comment(1)
nice solution but does not take into account linebreaks forced with \nRoswald
M
0
@IBOutlet weak var constraintTxtV: NSLayoutConstraint!
func TextViewDynamicallyIncreaseSize() {
    let contentSize = self.txtVDetails.sizeThatFits(self.txtVDetails.bounds.size)
    let higntcons = contentSize.height
    constraintTxtV.constant = higntcons
}
Machutte answered 24/11, 2017 at 10:7 Comment(3)
Your answer should not only consist of code, but also of an explanation regarding the code. Please see How to Answer for more details.Haydeehayden
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.Monolithic
This answer is incomplete. It refers to important variables who's types are unknown which defeats the purposeBraided
D
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)

            }
Decoteau answered 20/3, 2019 at 5:11 Comment(0)
S
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 
Succedaneum answered 11/11, 2019 at 5:32 Comment(0)
W
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
    }
Walter answered 11/7, 2022 at 16:55 Comment(0)
T
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)
    }
}
Trumpet answered 5/1, 2023 at 10:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.