5

I want labels to have a font size that is proportional to the size of the screen. I've subclassed the UILabel class to accomplish this:

@IBDesignable class MyCustomLabel: UILabel {
    override func layoutSubviews() {
        super.layoutSubviews()
        self.font = UIFont(name: "myFontName", size: (self.font?.pointSize)!)
        self.adjustsFontSizeToFitWidth = true
    }
}

The labels (which have a proportional width to superview constraint attached to them) resize correctly when the app is first launched, but when it enters background state layoutSubviews gets repeatedly called. The app no longer responses to users' input and keeps assigning a font size to the font.

Why is this happening?

Cesare
  • 9,139
  • 16
  • 78
  • 130

1 Answers1

8

In your test project, if I run on an iPhone, I'm not seeing layoutSubviews() being called in the background. It only happens on iPad.

That's because your app supports Multitasking:

  • When your app is deactivated, iOS resizes it into different sizes in order to take snapshots of it for the app switcher. Those resizes cause your view's layoutSubviews() to be called. That's completely normal.
  • iOS then returns your app to the original size.

The real problem is that you are creating a "layout loop". Your code in layoutSubviews() is causing your own view's layout to be invalidated, so the system needs to run the layout process again. Then layout runs, you do it again, and it happens all over again.

Specifically, the cause is:

self.font = UIFont(name: fontName, size: fontSize)

Changing your label's font causes its intrinsicSize to change, which means that its superviews may need their layout to be updated, so the layout process needs to run again. It's a bad idea to do this in layoutSubviews() because it causes layout loops. You should really only change properties of your subviews, not your view itself.

Why do you think you need to do this in layoutSubviews()? There is probably a better place to put it, outside of the layout process. In your example, I don't see how this code does anything useful at all.

It would make more sense to set adjustsFrameSizeToWidth once, and then don't do anything in layoutSubviews():

override init(frame: CGRect) {
    super.init(frame: frame)
    self.adjustsFontSizeToFitWidth = true
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    self.adjustsFontSizeToFitWidth = true
}

If you're trying to change your font's size depending on the size class, you can do it in code by overriding traitCollectionDidChange():

override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)

    var fontSize: CGFloat
    if (self.traitCollection.horizontalSizeClass == .Regular) {
        fontSize = 70
    }
    else {
        fontSize = 30
    }
    self.font = UIFont(name: fontName, size:fontSize)
}
Kurt Revis
  • 27,695
  • 5
  • 68
  • 74
  • Thanks Kurt! Is there a way to set the custom font programmatically in your solution? As you may know, custom fonts don't work well with Size Classes (http://stackoverflow.com/questions/26166737/custom-font-sizing-in-xcode6-size-classes-not-working-properly-w-custom-fonts for reference). – Cesare May 28 '16 at 05:16
  • You mean that custom fonts don't work well _with the size class support in Interface Builder_. Size classes are always present, you can always get them using `self.traitCollection.horizontalSizeClass` or `self.traitCollection.verticalSizeClass`, and you can always override `traitCollectionDidChange()` to use them. That's all the IB support does for you anyway. I'll add an example. – Kurt Revis May 28 '16 at 05:43
  • thanks for the great answer, Kurt!! a real problem, and you solved it. – Fattie Jul 31 '16 at 16:19