Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to fix UILabel intrinsicContentSize on iOS 11

On iOS 11 many of our layouts are breaking due to labels apparently misreporting their intrinsicContentSize.

The bug seems to manifest worst when a UILabel is wrapped in another view that attempts to implement intrinsicContentSize itself. Like so (simplified & contrived example):

class LabelView: UIView {

    let label = UILabel()

    override init(frame: CGRect) {

        super.init(frame: frame)

        self.setup()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    private func setup() {

        self.label.textColor = .black
        self.label.backgroundColor = .green
        self.backgroundColor = .red
        self.label.numberOfLines = 0
        self.addSubview(self.label)

        self.label.translatesAutoresizingMaskIntoConstraints = false
        self.label.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
        self.label.trailingAnchor.constraint(lessThanOrEqualTo: self.trailingAnchor).isActive = true
        self.label.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
        self.label.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
    }

    override var intrinsicContentSize: CGSize {

        let size = self.label.intrinsicContentSize

        print(size)

        return size
    }
}

The intrinsicContentSize of the UILabel is very distinctive and looks something like: (width: 1073741824.0, height: 20.5). This causes the layout cycle to give far too much space to the view's wrapper.

This only occurs when compiling for iOS 11 from XCode 9. When running on iOS 11 compiled on the iOS 10 SDK (on XCode 8).

On XCode 8 (iOS 10) the view is rendered correctly like so:

enter image description here

on XCode 9 (iOS 11) the view is rendered like this:

XCode 9 rendering

A Gist with full playground code demonstrating this issue is here.

I have filed a radar for this and have at least one solution to the problem (see answer below). I wonder if anyone else has had this problem or has alternative approached you might try.

like image 301
Sam Avatar asked Jan 11 '18 16:01

Sam


People also ask

What is intrinsicContentSize?

Intrinsic content size is information that a view has about how big it should be based on what it displays. For example, a label's intrinsic content size is based on how much text it is displaying. In your case, the image view's intrinsic content size is the size of the image that you selected.

How do I set intrinsic content size?

Setting the intrinsic content size of a custom view lets auto layout know how big that view would like to be. In order to set it, you need to override intrinsicContentSize . Whenever your custom view's intrinsic content size changes and the frame should be updated.

What is Uilabel?

A view that displays one or more lines of informational text.

How does a view's intrinsic content size aid in auto layout?

In general, the intrinsic content size simplifies the layout, reducing the number of constraints you need. However, using the intrinsic content size often requires setting the view's content-hugging and compression-resistance (CHCR) priorities, which can add additional complications.


1 Answers

So through experimenting on the playground I was able to come up with a solution that involves testing for the extremely large intrinsic content size.

I noticed that all UILabels that misbehave have numberOfLines==0 and preferredMaxLayoutWidth=0. On subsequent layout passes, UIKit sets preferredMaxLayoutWidth to a non-zero value, presumably to iterate onto the correct height for the label. So the first fix was to try temporarily setting numberOfLines when (self.label.numberOfLines == 0 && self.label.preferredMaxLayoutWidth == 0).

I also noticed that all UILabels that have these two properties as 0 do not necessarily misbehave. (i.e. the inverse isn't true). So this fix worked, but modified the label unnecessarily some of the time. It also has a small bug that when the label's text contains \n newlines, number of lines should be set to the number of lines in the string, not 1.

The final solution I came to is a little more hacky, but specifically looks for UILabel misbehaving and only kick's it then...

override var intrinsicContentSize: CGSize {

    guard super.intrinsicContentSize.width > 1000000000.0 else {

        return super.intrinsicContentSize
    }

    var count = 0

    if let text = self.text {

        text.enumerateLines {(_, _) in
            count += 1
        }

    } else {

        count = 1
    }

    let oldNumberOfLines = self.numberOfLines

    self.numberOfLines = count
    let size = super.intrinsicContentSize

    self.numberOfLines = oldNumberOfLines

    return size
}

You can find this as a Gist here.

like image 167
Sam Avatar answered Sep 30 '22 08:09

Sam