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:
on XCode 9 (iOS 11) the view is rendered like this:
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.
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.
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.
A view that displays one or more lines of informational text.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With