7

I am adding a UILabel to a view with the following layout requirements:

  • The label should be centered
  • The label should have a maximum font size of 100 points, but should scale down to fit. It should not truncate.
  • The label's height should not exceed 450 points.
  • Another view will be positioned directly below the label.

My label's properties layout constraints seem to describe this adequately:

let label = UILabel()
label.backgroundColor = UIColor.yellowColor()
label.font = UIFont.systemFontOfSize(100)
label.numberOfLines = 0
label.textAlignment = .Center
label.adjustsFontSizeToFitWidth = true
label.text = "What's Brewing in PET/CT: CT and MR Emphasis"
label.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(label)

view.addConstraint(NSLayoutConstraint(item: label, attribute: .CenterX, relatedBy: .Equal, toItem: view, attribute: .CenterX, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: label, attribute: .CenterY, relatedBy: .Equal, toItem: view, attribute: .CenterY, multiplier: 1, constant: 0))
view.addConstraint(NSLayoutConstraint(item: label, attribute: .Left, relatedBy: .Equal, toItem: view, attribute: .Left, multiplier: 1, constant: 60))
view.addConstraint(NSLayoutConstraint(item: label, attribute: .Right, relatedBy: .Equal, toItem: view, attribute: .Right, multiplier: 1, constant: -60))
view.addConstraint(NSLayoutConstraint(item: label, attribute: .Height, relatedBy: .LessThanOrEqual, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 450))

However, I am finding that with longer content like that below, the label is displayed with some unexpected margins:

enter image description here

This is unacceptable since any views placed directly above or below the label will have a gap.

Based on the discussion below, my working theory is that the layout engine is calculating the size based on the font size of 100, without taking into account the subsequent scaling when the content is longer.

How can I meet the layout requirements listed above (ideally without using any deprecated methods)?

Here's a test project to play with.

Ben Packard
  • 26,102
  • 25
  • 102
  • 183
  • 1
    You seem to be setting the height to <= 450. From your images it looks like the UILabel is not feeling obliged to shrink in height once it has a certain size. It is then centering the text vertically in the view. Have you tried removing the height constraint totally? It should let you away without having it as it knows the text field is centered. If it complains, try changing the constraint to >= 0 rather than <= 450. – Rory McKinnel May 05 '15 at 19:59
  • Yes, that is required (I will update my question to mention it) - I don't want the label growing any taller than 450, but it can and should be as short as possible. My understanding was that the hugging priority was meant to resolve this. – Ben Packard May 05 '15 at 20:05
  • When you set the text/font, you could try calling `[label sizeToFit];`. Not a swift person. I guess it would be `label.sizeToFit()`. – Rory McKinnel May 05 '15 at 20:16
  • Thanks but I've tried that (along with setNeedsUpdateConstraints and setNeedsLayout) – Ben Packard May 05 '15 at 20:19
  • when you use the .XXXXThanOrEqual constraint, it often helps to use another constraint with .Equal and a lower priority so you tell the layout manager in which if direction to scale. So in your case try Height Equal with 0 and set priority to low – Simon Meyer May 05 '15 at 20:19
  • Also try setting `label.preferredMaxLayoutWidth` to the width of the area. This can affect the height calculation. – Rory McKinnel May 05 '15 at 20:26
  • @SimonMeyer thanks, already tried that one. – Ben Packard May 05 '15 at 20:32
  • @RoryMcKinnel yep, tried that one too. Thanks though. – Ben Packard May 05 '15 at 20:32
  • I also added a demo project for anyone who wants to quickly try this out. – Ben Packard May 05 '15 at 20:35
  • If you remove the height constraint does it work? If it works, you could embed the text area in a view pinned to 0 on all sides and put your text view constraints on the view instead. A UIView may behave better with respect to height and still give you that 450 restriction you want. – Rory McKinnel May 05 '15 at 20:36
  • Your font is just too big. Posted tests as an answer. – Rory McKinnel May 05 '15 at 20:48
  • That's crafty - yes, without the height constraint the label height is calculated correctly, but unfortunately embedding it in a view didn't help. – Ben Packard May 05 '15 at 20:52
  • just comment `label.adjustsFontSizeToFitWidth = true` and see what happens... – Bhavin Bhadani May 06 '15 at 06:07
  • @Bhavin This leads to truncation. – Ben Packard May 06 '15 at 19:00
  • @BenPackard because of that line it automatically adjust font size...let say you gave 100 but to adjust the label it want 70 ...so it adjust its font to 70...here is the problem start...as view adjust its height according to font size 100 not with 70... – Bhavin Bhadani May 07 '15 at 04:40
  • Right - that's the problem. See also the discussion under Rory's answer. – Ben Packard May 07 '15 at 15:23
  • @Ben Packard, i am not able to download your source code. can you look over it? – Jatin Patel - JP May 12 '15 at 05:13
  • @None I'm able to download it without issue, and seems that other are too. – Ben Packard May 12 '15 at 12:19
  • @BenPackard, now i am able to download. it seems like network issue – Jatin Patel - JP May 12 '15 at 12:21
  • http://stackoverflow.com/questions/3669844/how-to-get-uilabel-uitextview-auto-adjusted-font-size/6141770#6141770 check this – Kiran P Nair May 13 '15 at 06:56

2 Answers2

1

Had a look at your test project you kindly added.

The problem is that the font you are setting is too big (100pt) and the 450 point height can not be satisfied. You have set label.adjustsFontSizeToFitWidth = true which is causing it to try and fit it in as best it can by using a smaller font.

So when you set the font to 100, it is sizing the text area based on fitting the original text truncated and then it compresses it by changing the font to fit the width. This leaves a gap at the top and bottom after the compression as the size was based on the original font.

If you use smaller sizes like 90, it works fine as it fits in less than 450.

If you remove adjustsFontSizeToWith it works fine and fills the space, but it truncates the text.

If you remove the height constraint it works fine, but its height goes > 450.

So your problem is simply that the test font is too big for 450 height.

Possible Solution:

Ask the label for the font size needed using the answer from here: How to get UILabel (UITextView) auto adjusted font size?

Basically you use:

CGFloat actualFontSize;
[label.text sizeWithFont:label.font
             minFontSize:label.minimumFontSize
          actualFontSize:&actualFontSize
                forWidth:label.bounds.size.width
           lineBreakMode:label.lineBreakMode];

This is deprecated but that does not mean you cant use it for now. You need to convert it to swift.

Call this and if the font size is less than the original, set the font in the label the lower size. This in theory should give you a perfect fit as it should resize to the new font size you set.

Possible Solution Update:

I had a search for the swift equivalent of sizeWithFont and there are many articles but none I could see which replaced the version of the function noted above.

A slightly inefficient solution would be to add the following before your addSubview code in the test project you posted(excuse my swift). It basically searches backwards in font size until the font fits the known limits. This could obviously be made more efficient, but it shows that if you calculate the correct font before layout, then it will all fit perfectly at 450.0 height.

Tested this in your project and the text fits perfectly in the event of it originally not fitting at 450.0

    // The current font size and name
    var fontSize:CGFloat = 120
    let fontName:String = "AvenirNext-Regular"

    // The size to fit is the frame width with 60 margin each size and height
    // of 450
    let fitSize:CGSize = CGSize(width:view.frame.width-120.0,height:450)
    var newSize:CGSize

    do{
        // Create the trial font
        label.font = UIFont(name: fontName, size: fontSize)

        // Calculate the size this would need based on the fitSize we want
        newSize = label.sizeThatFits(fitSize)

        // Make the font size smaller for next time around.
        fontSize-=0.5
    } while (newSize.height >= 450)

    println("Font size had to be reduced to \(fontSize)")

    view.addSubview(label)
Community
  • 1
  • 1
Rory McKinnel
  • 7,936
  • 2
  • 17
  • 28
  • But the text for the label is variable and when short 100 points is the suitable size. Seems like that's the whole point of `adjustsFontSizeToFitWidth`? Shouldn't the 'fitting' calculation work together with the adjustFontSize value rather than assuming truncation? Changing the font size to 90 will reproduce the same issue for the longer strings I need to accommodate. – Ben Packard May 05 '15 at 20:58
  • 1
    You would hope so, but seems not to be the case. I think it is because it is visually shrinking it to fit the width and not the height. I updated my answer with a possible solution. I think the only way to get it to work is to adjust the font size when its too big for 450 yourself. – Rory McKinnel May 05 '15 at 21:00
  • Just saw your edit with the possible solution, thanks. I will try it out. Also considering just setting the number of lines to 3 or 4 instead of 0, and removing the height constraint. – Ben Packard May 05 '15 at 21:01
  • @BenPackard Yes, you really only have three options: Either scale the font to make it fit as described, relax your height constraint or restrict the number of lines. Restricting the lines will cause it to truncate the text though which may not be a good thing. – Rory McKinnel May 16 '15 at 10:35
  • @BenPackard Posted another update with swift code that will work. Tested it in your project. Add the code before you add the subview. Can be made more efficient, but shows the general approach will work. – Rory McKinnel May 16 '15 at 16:52
1

I took a look at your project and I might have found out a solution to your problem.

I added the following to the label setup:

label.minimumScaleFactor = 0.5

This is how the view looks like after my change: enter image description here

There is no more spacing on top of the label.

Hope this helps you with your problem! Let me know how it went.

Catalina T.
  • 3,456
  • 19
  • 29
  • Thanks but I think that is just a lucky break with this particular string - for example, try swapping out the label text for `"Current Controversies, Problems, and Techniques in Nuclear Medicine 2014 - Part I"` and the issue persists. It *is* strange to me, though, that setting a `minimumScaleFactor` would change the layout from three to four lines. If anything, since the font size can only be reduced (or remain the same) with the addition of this, I would expect the label to break over fewer or the same number of lines - not more. Maybe that help will point someone in the right direction. – Ben Packard May 12 '15 at 12:18
  • Yeah, you're right. However, if you change the value for `minimumFontScale`, it will work also. But, it is clear this is not supposed to be like this. I found this in the `UILabel` class, while looking into your problem `var adjustsFontSizeToFitWidth: Bool // default is NO // deprecated - hand tune by using NSKernAttributeName to affect tracking // NOTE: deprecated - use minimumScaleFactor. default is 0.0`, so maybe it is a bug in the framework – Catalina T. May 13 '15 at 11:14