Search code examples
iosswiftuilabel

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


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.


Solution

  • 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"