Allow label to use preferred width necessary to not character wrap (when word wrap is enabled)

59 views Asked by At

I have a UILabel with three lines set to word wrap. I also set the label to have a width of 180. While this works in almost all circumstances, some languages with very long words are starting to character wrap because their length is greater than 180.

Ideally, I would have the label keep the width of 180 in all cases, except if a word is too long to fit. Then, I would like to expand the width to be the minimum size to keep the longest word in tact. How can I do that?

let myString = "thisisaveryveryveryveryverylongstring"
let myLabel = UILabel()
myLabel.text = myString
mylabel.numberOfLines = 3
myLabel.lineBreakMode = .byWordWrapping
myLabel.widthAnchor.constraint(equalToConstant: 180).isActive = true

I've tried setting a preferredWidth as well as setting the width constraint to have a priority of less than 1000, but in both circumstances the longer word still character wraps.

1

There are 1 answers

0
DonMag On BEST ANSWER

It's not entirely clear what you are trying to do, but I'm going to guess it's like this...

Consider the different word lengths here:

English: Some Occupation
German:  Irgendeine Beschäftigung


English: Occupational Therapist 
German:  Beschäftigungstherapeutin

Using the first example, if the string is: "Some Occupation Irgendeine Beschäftigung" and the label width is constrained to 180-points, it will look like this:

enter image description here

Which, I'm guessing, is the first part of your goal -- 180-point label frame width.

However, using the second example, if the string is: "Occupational Therapist Beschäftigungstherapeutin" and the label width is constrained to 180-points, it will look like this:

enter image description here

The single word "Beschäftigungstherapeutin" by itself is too wide, and we get character wrapping.

So, we need to get to here:

enter image description here

The label width is now 206-points.

To accomplish that, we can split the string into individual words and find the widest single word:

func calcMaxWordWidth(_ str: String, forLabel: UILabel) -> CGFloat {
    var maxWidth: CGFloat = 0

    let words = str.components(separatedBy: " ")

    words.forEach { s in
        forLabel.text = s
        forLabel.sizeToFit()
        maxWidth = max(maxWidth, forLabel.frame.width)
    }

    return maxWidth
}

Here's an example you can try:

class ExampleVC: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        var myString: String = ""
        
        var myLabelA: UILabel = UILabel()
        var myLabelB: UILabel = UILabel()
        
        var sizingLabel: UILabel = UILabel()
        
        // all labels have the same font
        [myLabelA, myLabelB, sizingLabel].forEach { v in
            v.font = .systemFont(ofSize: 17.0)
        }

        myLabelA.numberOfLines = 3
        myLabelA.lineBreakMode = .byWordWrapping
        
        myLabelB.numberOfLines = 3
        myLabelB.lineBreakMode = .byWordWrapping
        
        // so we can see the label frames
        myLabelA.backgroundColor = .yellow
        myLabelB.backgroundColor = .green

        myString = "Occupational Therapist Beschäftigungstherapeutin"
        //myString = "Some Occupation Irgendeine Beschäftigung"

        myLabelA.text = myString
        myLabelB.text = myString

        myLabelA.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myLabelA)
        
        myLabelB.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(myLabelB)
        
        // for sizingLabel
        //  do NOT set .translatesAutoresizingMaskIntoConstraints = false
        //  do NOT add it as a subview
        //  set number of lines to 1
        sizingLabel.translatesAutoresizingMaskIntoConstraints = true
        sizingLabel.numberOfLines = 1
        
        let maxWordWidth: CGFloat = calcMaxWordWidth(myString, forLabel: sizingLabel)
        let actualWidth: CGFloat = max(180.0, maxWordWidth)

        print("mx:", maxWordWidth, "act:", actualWidth)
        
        // set myLabelA width to 180.0
        myLabelA.widthAnchor.constraint(equalToConstant: 180.0).isActive = true
        
        // set myLabelB width to MAX of 180.0 or widest single word
        myLabelB.widthAnchor.constraint(equalToConstant: actualWidth).isActive = true

        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            myLabelA.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            myLabelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            
            myLabelB.topAnchor.constraint(equalTo: myLabelA.bottomAnchor, constant: 20.0),
            myLabelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            
        ])
        
    }
    
    func calcMaxWordWidth(_ str: String, forLabel: UILabel) -> CGFloat {
        var maxWidth: CGFloat = 0

        let words = str.components(separatedBy: " ")

        words.forEach { s in
            forLabel.text = s
            forLabel.sizeToFit()
            maxWidth = max(maxWidth, forLabel.frame.width)
        }

        return maxWidth
    }
}

Please note: this is meant to be a starting point. You would probably want to implement a "max width" so your label doesn't extend wider than the screen (or into another view). Also, I did very little testing of this... so it should not be considered "Production Ready"