How to make multi-line UILabel text fit within predefined width without wrapping mid-word
Asked Answered
C

3

6

I have a UILabel carefully laid out in Interface Builder with proper height and width constraints. The number of lines is set to 4. The wrapping is set to word wrap. The text is "CHECKED". The font size is very large and thus it only fits "CHECKE" and the "D" is on the second line. Writing "Checked" instead of "CHECKED" lets the font shrink (as intended) so that the whole word fits. But (the text is user given and it can be expected that the user writes fully uppercase words) having uppercase words the label does not break it/shrink the font as expected.

Do you have a suggestion as to what I might have missed? Capitalising the words (thusly only having the first letter uppercase) does work, but is not what the client wants.

Updated question

The problem seems to be unrelated to having uppercase or lowercase text. My problem could be solved by an answer to the following question:

How to make (ideally with the help of only Interface Builder) the UILabel text shrink trying to fit full words within all available lines without wrapping the text mid-word?

  • If the text "CHECKED" is too wide for a label (with more than 1 line available) it should shrink the font size instead of breaking the "D" and wrapping the single letter to the next line.
  • If the text is "one CHECKED two" and the single word "CHECKED" is already too wide for a label (with more than 1 line available) it should break between all words and shrinking the font size so that "CHECKED" still fits the middle line.

Avoiding:

one
CHECKE
D two

Thank you very much!

Cutty answered 19/3, 2017 at 19:10 Comment(0)
O
3

Here is a UILabel subclass that will find the largest word in the labels text, use the boundingRect function of NSString to see how large that one word will be with the current font, and drop the font size until it fits the width.

class AutosizingMultilineLabel: UILabel {
    override func layoutSubviews() {
        super.layoutSubviews()
        self.adjustFontToFitWidth()
    }
    
    func adjustFontToFitWidth() {
        guard let currentFont = self.font else { return }
        let minimumFontSize: CGFloat = floor(self.minimumScaleFactor * currentFont.pointSize)
        
        var newFontSize = currentFont.pointSize
        var theNewFont = currentFont
        
        if let text = self.text, let longestWord = text.components(separatedBy: " ").max(by: {$1.count > $0.count})?.replacingOccurrences(of: "\n", with: "") {
            let nsString = longestWord as NSString
            
            while newFontSize > minimumFontSize {
                theNewFont = currentFont.withSize(newFontSize)
                
                let boundingRect = nsString.boundingRect(with: CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude),
                                                         options: NSStringDrawingOptions.usesLineFragmentOrigin,
                                                         attributes: [.font: theNewFont],
                                                         context: nil)
                
                if ceil(boundingRect.size.width) <= self.bounds.size.width {
                    break
                }
                newFontSize -= 1
            }
            
            self.font = theNewFont
        }
    }
}
Olivann answered 23/3, 2021 at 3:17 Comment(2)
This is actually quite nice. Though I would warn that it is determining the longest word by character count, when "WOW' is longer in the horizontal dimension than "jilt" for example. Also, fractional font sizes are supported so I would modify newFontSize with a multiplier like newFontSize *= 0.95 instead of subtracting 1. The difference in length between font size 10 and 9 appears visually to be larger than between 20 and 19, even thought they're the same difference. Then you don't need to floor the minimum font size either. The while should probably be >= rather than just >Entelechy
this is the only solution that actually worked for me, sending love in your direction, Ric! thanksMoises
E
0

When the word is bigger than the line, word wrap doesn't work. If it doesn't fit on this line, it won't fit on the next line. (same word, same size, same line size). To make it fit, the label will start putting letters on the next line.

If you allow multiple lines on your label, the OS will try to fill the lines before adjusting the font size.

Expect answered 19/3, 2017 at 19:21 Comment(3)
It works the way I want with "Checked" which is also bigger than the line. It shrinks the font size instead of putting letters on the next line. How to make it do the same with "CHECKED"?Cutty
I can't seem to replicate this effect. If the word is bigger than the line and the label still has lines available, it will break the word apart by letter, before shrinking the font size. (Whether I used "Checked" or "CHECKED")Expect
When following the example of @DonMag, the UILabel seems to do the same thing for both "Checked" and "CHECKED". For some reason it doesn't work with my setup. I suppose "Checked" is slightly narrower and simply does fit the width of the UILabel in full font size. I will update the question: Is there a way to do font size auto-shrinking without putting single letters of a word into the next line?Cutty
E
0

I think you're just running into a limitation on Autoshrink.

In Interface Builder:

  • add a new UILabel with Width: 230 and Height: 280
  • set the Font to System 44.0
  • set Line Break: Truncate Tail
  • set Autoshrink: Minimum Font Scale at 0.15
  • set the text of the label to test CHECKED lines

Now, drag the handle on the right edge of the label left and right... when it gets too narrow, the word CHECKED will break onto the next line.

Change CHECKED to checked and do the same thing. You should see the same behavior.

Now, try dragging the Bottom edge up and down. With either CHECKED or checked, you should see the Font Size auto shrink.

So... to do what you're trying to do, you might have to skip Autoshrink and instead do some code calculations.

Edit: further visual of what goes on...

  • Start with above values, but set the Height of the label to 170 - gives it just a little vertical padding.
  • Now, drag the left edge to make it narrower.
  • When you reach the end of the word CHECKED, and keep going, you will see the font shrink until it gets small enough that there is space for it to wrap to a 4th line.

I think you're going to need some code to get exactly what you need.

Eventuate answered 19/3, 2017 at 21:55 Comment(2)
Thank you! I have updated my question (see above). Is there a way to avoid wrapping mid-word?Cutty
@Dr.Cashberg - I've edited my answer... Looks like you'll need code in addition to Constraints and IB settings.Eventuate

© 2022 - 2024 — McMap. All rights reserved.