How to calculate the width of a text string of a specific font and font-size?

Viewed 126854

I have a UILabel that displays some chars. Like "x", "y" or "rpm". How can I calculate the width of the text in the label (it does not ues the whole available space)? This is for automatic layouting, where another view will have a bigger frame rectangle if that UILabel has a smaller text inside. Are there methods to calculate that width of the text when a UIFont and font size is specified? There's also no line-break and just one single line.

14 Answers

Update Sept 2019

This answer is a much cleaner way to do it using new syntax.

Original Answer

Based on Glenn Howes' excellent answer, I created an extension to calculate the width of a string. If you're doing something like setting the width of a UISegmentedControl, this can set the width based on the segment's title string.

extension String {

    func widthOfString(usingFont font: UIFont) -> CGFloat {
        let fontAttributes = [NSAttributedString.Key.font: font]
        let size = self.size(withAttributes: fontAttributes)
        return size.width
    }

    func heightOfString(usingFont font: UIFont) -> CGFloat {
        let fontAttributes = [NSAttributedString.Key.font: font]
        let size = self.size(withAttributes: fontAttributes)
        return size.height
    }

    func sizeOfString(usingFont font: UIFont) -> CGSize {
        let fontAttributes = [NSAttributedString.Key.font: font]
        return self.size(withAttributes: fontAttributes)
    }
}

usage:

    // Set width of segmentedControl
    let starString = "⭐️"
    let starWidth = starString.widthOfString(usingFont: UIFont.systemFont(ofSize: 14)) + 16
    segmentedController.setWidth(starWidth, forSegmentAt: 3)

Swift-5

Use intrinsicContentSize to find the text height and width.

yourLabel.intrinsicContentSize.width

This will work even you have custom spacing between your string like "T E X T"

Oneliner in Swift 4.2

let size = "abc".size(withAttributes:[.font: UIFont.systemFont(ofSize: 18.0)])

If you're struggling to get text width with multiline support, so you can use the next code (Swift 5):

func width(text: String, height: CGFloat) -> CGFloat {
    let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 17)
    ]
    let attributedText = NSAttributedString(string: text, attributes: attributes)
    let constraintBox = CGSize(width: .greatestFiniteMagnitude, height: height)
    let textWidth = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).width.rounded(.up)

    return textWidth
}

And the same way you could find text height if you need to (just switch the constraintBox implementation):

let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude)

Or here's a unified function to get text size with multiline support:

func labelSize(for text: String, maxWidth: CGFloat, maxHeight: CGFloat) -> CGSize {
    let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 17)
    ]

    let attributedText = NSAttributedString(string: text, attributes: attributes)

    let constraintBox = CGSize(width: maxWidth, height: maxHeight)
    let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral

    return rect.size
}

Usage:

let textSize = labelSize(for: "SomeText", maxWidth: contentView.bounds.width, maxHeight: .greatestFiniteMagnitude)
let textHeight = textSize.height.rounded(.up)
let textWidth = textSize.width.rounded(.up)

For Swift 5.4

extension String {
    func SizeOf_String( font: UIFont) -> CGSize {
        let fontAttribute = [NSAttributedString.Key.font: font]
        let size = self.size(withAttributes: fontAttribute)  // for Single Line
       return size;
    }
}

Use it like...

        let Str = "ABCDEF"
        let Font =  UIFont.systemFontOfSize(19.0)
        let SizeOfString = Str.SizeOfString(font: Font!)

Swift 4

extension String {
    func SizeOf(_ font: UIFont) -> CGSize {
        return self.size(withAttributes: [NSAttributedStringKey.font: font])
    }
}

Not sure how efficient this is, but I wrote this function that returns the point size that will fit a string to a given width:

func fontSizeThatFits(targetWidth: CGFloat, maxFontSize: CGFloat, font: UIFont) -> CGFloat {
    var variableFont = font.withSize(maxFontSize)
    var currentWidth = self.size(withAttributes: [NSAttributedString.Key.font:variableFont]).width

    while currentWidth > targetWidth {
        variableFont = variableFont.withSize(variableFont.pointSize - 1)
        currentWidth = self.size(withAttributes: [NSAttributedString.Key.font:variableFont]).width
    }

    return variableFont.pointSize
}

And it would be used like this:

textView.font = textView.font!.withSize(textView.text!.fontSizeThatFits(targetWidth: view.frame.width, maxFontSize: 50, font: textView.font!))

The way I am doing it my code is to make an extension of UIFont: (This is Swift 4.1)

extension UIFont {


    public func textWidth(s: String) -> CGFloat
    {
        return s.size(withAttributes: [NSAttributedString.Key.font: self]).width
    }

} // extension UIFont

SwiftUI with Swift5

In SwiftUI, you could not find an easy way to convert UIFont to Font. So the previous answers may not work. You could use GeometryReader{ geometryProxy in } inside the overlay() modifier to get the Text size. Be careful that if you don't use it inside overlay(), the View will expand to as much as it can.

If you want to pass the variable out, you may need to write a View extension function to do so.

Text("Lat.: \(latitude), Lon.: \(longitude) ")
    .font(Font.custom("ChalkboardSE-Light",size: 20))
    .overlay(
    GeometryReader{ geometryProxy in
        Image("button_img")
             .resizable()
             .frame(width: 10 , height: 10)
             .offset(x: geometryProxy.size.width)
             .extensionFunction{ //... }
              })
    

I copied the extension function from another thread a couple days ago.

extension View{
    func extensionFunction(_ closure:() -> Void) -> Self{
        closure()
        return self
    }
    
}
Related