How to fix UILabel intrinsicContentSize on iOS 11 - ios

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.

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.

Related

How to set UIView height equal to max of two subviews UILabel?

I have to UILabel with dynamic height. I want to set it superview height equal to max of UILabel heights.
class ComponentCell: UIView {
private lazy var leftRow: UILabel = UILabel()
private lazy var rightRow: UILabel = UILabel()
init(leftValue: String, rightValue: String) {
super.init(frame: .zero)
leftRow.backgroundColor = .red
leftRow.numberOfLines = 0
leftRow.lineBreakMode = .byWordWrapping
leftRow.text = leftValue
rightRow.text = rightValue
rightRow.backgroundColor = .yellow
rightRow.numberOfLines = 0
rightRow.lineBreakMode = .byWordWrapping
self.addSubview(self.leftRow)
self.addSubview(self.rightRow)
leftRow.sizeToFit()
rightRow.sizeToFit()
leftRow.setContentHuggingPriority(.required, for: .vertical)
rightRow.setContentHuggingPriority(.required, for: .vertical)
self.translatesAutoresizingMaskIntoConstraints = false
self.leftRow.snp.makeConstraints { make in
make.top.equalToSuperview()
make.left.equalToSuperview()
make.width.equalToSuperview().dividedBy(2)
}
self.rightRow.snp.makeConstraints { make in
make.top.equalToSuperview()
make.right.equalToSuperview()
make.width.equalToSuperview().dividedBy(2)
}
self.layoutIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If I set leftRow.botton.equalTo(superview.bottom) and rightRow.botton.equalTo(superview.bottom) it's working. But I think is not a good way. And I don't understand why setContentHuggingPriority not helped me to solve this problem.
Content Hugging
Content hugging leads more likely to squeeze your labels. What you want is the height of the labels to be more respected. So you'd rather use compression resistance priority. However you actually need neither of those.
Layout Constraints
Since you're setting your constraints programatically, you'll need to set translatesAutoresizingMaskIntoConstraints to false for your labels as well:
leftRow.translatesAutoresizingMaskIntoConstraints = false
rightRow.translatesAutoresizingMaskIntoConstraints = false
The bottom constraint is actually a good start, but you don't want to fit the height of the smaller label unnecessarily to the height of the bigger label. So you would want to add a constraint that is "less than or equal to the bottom anchor":
make.bottom.lessThanOrEqualTo(self.snp.bottom)
Lazy Variables
If you want to use lazy variables you'll have to change the way there being initialized. The way you've written it, it initializes the variables right away when initializing the class. But you only want them to be initialized when they're used the first time. For that you need to write it like this:
private lazy var leftRow: UILabel = {
return UILabel()
}()
private lazy var rightRow: UILabel = {
return UILabel()
}()
However in your case you don't need lazy loading, so you can initialize them directly:
private let leftRow = UILabel()
private let rightRow = UILabel()
Other
Since you're using layout constraints, you don't need to call sizeToFit on the labels. It doesn't do anything.
Calling layoutIfNeeded() within the init doesn't do anything either since it will be called anyway once you add ComponentCell as a subview to another view.

programmatically set a button width and height

I would like to programmatically customize the UIButton. My code starts here:
class MyButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowRadius = 5.0
...
}
}
Now I would like to define a constant width and height for the button, how to do it in code?
I would recommend to use autolayout:
class MyButton: UIButton {
override func awakeFromNib() {
super.awakeFromNib()
layer.shadowRadius = 5.0
// autolayout solution
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: 200).isActive = true
self.heightAnchor.constraint(equalToConstant: 35).isActive = true
}
}
You need to override override init(frame: CGRect) method
class MyButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
// Set your code here
let width = 300
let height = 50
self.frame.size = CGSize(width: width, height: height)
backgroundColor = .red
layer.shadowRadius = 5.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
If your button is having constraints set from the storyboard as below and you want to change the width of the button, then this answer is helpful.
constraints set from the storyboard
Safe Area.trailing = Button.trailing + 20
Button.leading = Safe Area.leading + 20
Safe Area.bottom = Button.bottom + 20
height = 40
see the image for a better understanding.
Requirement :
if #condition 1 gets satisfied, then change button width to 100 or any width dimension.
else
if #condition 2 gets satisfied, then keep width as it is ( as per given constraints)
To handle this,
Create an IBOutlet of leading and trailing constraints of that button.
Set it to inactive.
Add width anchor for the button programmatically.
Setting it inactive is mandatory because both won't work at the same time, so be careful.
#IBOutlet weak var btnNextTrailingConstraint: NSLayoutConstraint!
#IBOutlet weak var btnNextLeadingConstraint: NSLayoutConstraint!
if (condition1) {
btnNextLeadingConstraint.isActive = false
btnNextTrailingConstraint.isActive = false
btnNext.widthAnchor.constraint(equalToConstant: 100).isActive = true
}
else {
btnNextLeadingConstraint.isActive = true
btnNextTrailingConstraint.isActive = true
}
swift ios autolayout constraints
I tried finding a way to create a square button box. Hope I`m not at the wrong place here, but when I tried to find my own solution it was as easy as this:
button.frame.size.width = 200
button.frame.size.height = 200
And this works of course with all the other views.

Swift: Apply CGSize to Button Class not changing

I am trying to add all style info into a class and then subclass a UIButton to avoid duplication of code.
At the moment, my class looks like:
class CustomButton: UIButton {
required init() {
super.init(frame: .zero)
// set other operations after super.init, if required
backgroundColor = .red
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
frame.size = CGSize(width: 700, height: 100)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In the viewDidLoad I am adding:
let b1 = CustomButton()
view.addSubview(b1)
// auto layout
b1.translatesAutoresizingMaskIntoConstraints = false
b1.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
b1.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
As you can see in the class I have set the frame.size
frame.size = CGSize(width: 700, height: 100)
However, when I run it, It looks like so:
The width is clearly not 700. Any suggestions where I am going wrong?
The problem is you are mixing frame and auto layout. Specifically, once you turn off autoresizing, the frames go away. Why not do 100% auto layout? In fact, why not make centerX/centerY part of the init()?
class CustomButton: UIButton {
required init(width:CGFloat, height:CGFloat, centerButton:Bool) {
super.init(frame: .zero)
// set other operations after super.init, if required
backgroundColor = .red
layer.cornerRadius = 5
layer.borderWidth = 1
layer.borderColor = UIColor.black.cgColor
self.translatesAutoresizingMaskIntoConstraints = false
self.widthAnchor.constraint(equalToConstant: width).isActive = true
self.heightAnchor.constraint(equalToConstant: height).isActive = true
if centerButton {
self.centerXAnchor.constraint(equalTo: superview?.centerXAnchor).isActive = true
self.centerYAnchor.constraint(equalTo: superview?.centerYAnchor).isActive = true
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
And change your call in viewDidLoad() to:
let b1 = CustomButton(width:700, height:100, centerButton:true)
(I added the width/height specs to init() to make your code more flexible.)
EDIT: Regards to my last (parenthesized) statement. If all you do is replace:
frame.size = CGSize(width: 700, height: 100)
with:
self.widthAnchor.constraint(equalToConstant: 700).isActive = true
self.heightAnchor.constraint(equalToConstant: 100).isActive = true
Everything will work. But, in your question you also said that (emphasis mine):
I am trying to add all style info into a class and then subclass a
UIButton to avoid duplication of code.
While the code is untested, I added parameters to the init in an effort to make the code more adaptable to creating buttons on the fly. Depending on your needs, you can extend this to everything from backgroundColor to cornerRadius.
I come from an OOP background (actually a top-down back in the 70s), so subclassing is way too intuitive to me. What I presented was just that - subclass to avoid duplication of code. Swift presents new ways - specifically extension and convenience init. I think both of these would even work for you. I'm not sure of the specific pros/cons of extension versus subclassing - my feeling is that duplication of code is about the same (technically) for both - but I'll always appreciate what a "modern" language brings to a developer's toolkit!
To decide any views position or size ,you can use either
Frames based layout or constrain based Autolayout not both together.
Thanks

Slow load time for custom UIView in Swift

Background
In order to make a text view that scrolls horizontally for vertical Mongolian script, I made a custom UIView subclass. The class takes a UITextView, puts it in a UIView, rotates and flips that view, and then puts that view in a parent UIView.
The purpose for the rotation and flipping is so that the text will be vertical and so that line wrapping will work right. The purpose of sticking everything in a parent UIView is so that Auto layout will work in a storyboard. (See more details here.)
Code
I got a working solution. The full code on github is here, but I created a new project and stripped out all the unnecessary code that I could in order to isolate the problem. The following code still performs the basic function described above but also still has the slow loading problem described below.
import UIKit
#IBDesignable class UIMongolTextView: UIView {
private var view = UITextView()
private var oldWidth: CGFloat = 0
private var oldHeight: CGFloat = 0
#IBInspectable var text: String {
get {
return view.text
}
set {
view.text = newValue
}
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect){
super.init(frame: frame)
}
override func sizeThatFits(size: CGSize) -> CGSize {
// swap the length and width coming in and going out
let fitSize = view.sizeThatFits(CGSize(width: size.height, height: size.width))
return CGSize(width: fitSize.height, height: fitSize.width)
}
override func layoutSubviews() {
super.layoutSubviews()
// layoutSubviews gets called multiple times, only need it once
if self.frame.height == oldHeight && self.frame.width == oldWidth {
return
} else {
oldWidth = self.frame.width
oldHeight = self.frame.height
}
// Remove the old rotation view
if self.subviews.count > 0 {
self.subviews[0].removeFromSuperview()
}
// setup rotationView container
let rotationView = UIView()
rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
rotationView.userInteractionEnabled = true
self.addSubview(rotationView)
// transform rotationView (so that it covers the same frame as self)
rotationView.transform = translateRotateFlip()
// add view
view.frame = rotationView.bounds
rotationView.addSubview(view)
}
func translateRotateFlip() -> CGAffineTransform {
var transform = CGAffineTransformIdentity
// translate to new center
transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
// rotate counterclockwise around center
transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
// flip vertically
transform = CGAffineTransformScale(transform, -1, 1)
return transform
}
}
Problem
I noticed that the custom view loads very slowly. I'm new to Xcode Instruments so I watched the helpful videos Debugging Memory Issues with Xcode and Profiler and Time Profiler.
After that I tried finding the issue in my own project. It seems like no matter whether I use the Time Profiler or Leaks or Allocations tools, they all show that my class init method is doing too much work. (But I kind of knew that already from the slow load time before.) Here is a screen shot from the Allocations tool:
I didn't expand all of the call tree because it wouldn't have fit. Why are so many object being created? When I made a three layer custom view I knew that it wasn't ideal, but the number of layers that appears to be happening from the call tree is ridiculous. What am I doing wrong?
You shouldn't add or delete any subview inside layoutSubviews, as doing so triggers a call to layoutSubviews again.
Create your subview when you create your view, and then only adjust its position in layoutSubviews rather than deleting and re-adding it.

iOS AutoLayout working with something wrong

I add a UIView in UIStoryboard and bind it to a custom UIView class called testView,next, I create a UIView called circelView in textView in require init function ,then I crate two UILabel in circelView
this is my step
initialization the circelView
add AutoLayout to circelView
create two UILabel and add them to circelView
add AutoLayout to two UILabel
then I run the app, I can find the subView in current position , But I can not find the two UILabel in screen ,what happen?
this is my code :
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
circelView = UIView()
self.addSubview(circelView)
circelView.snp_makeConstraints(closure: { (make) -> Void in
make.size.equalTo(80)
make.top.equalTo(self.snp_top)
make.right.equalTo(self.snp_right)
})
titleLabel = UILabel()
titleLabel.textColor = colorffffff
titleLabel.backgroundColor = UIColor.yellowColor()
titleLabel.font = font32
titleLabel.text = "hello"
titleLabel.numberOfLines = 1
titleLabel.backgroundColor = UIColor.yellowColor()
titleLabel.textAlignment = NSTextAlignment.Center
circelView.addSubview(titleLabel)
titleLabel.snp_makeConstraints { (make) -> Void in
make.center.equalTo(circelView.snp_center)
}
detailLabel = UILabel()
detailLabel.textColor = colorffffff
detailLabel.font = font24
detailLabel.text = "hello"
titleLabel.numberOfLines = 1
detailLabel.textAlignment = NSTextAlignment.Center
detailLabel.addSubview(titleLabel)
detailLabel.snp_makeConstraints { (make) -> Void in
make.top.equalTo(titleLabel.snp_bottom).offset(3)
make.centerX.equalTo(titleLabel.snp_centerX)
}
}
A few things could be causing this:
How big is your font? If your font is too large, and your view is too small (80x80 in this case), the text may not be rendered.
In Autolayout, it is optimal to have views have sizes based on their content, and not explicit (i.e. 80x80). By having your labels be pinned to their superview on all edges, you can have the size of your 'circelView' be dynamic to its content (the two labels). To maintain the 'circle' aspect of your 'circelView', you can constraint the height of your 'circelView' to be the same as its width.
At the end of the day, this needs more debugging. I advise examining the view hierarchy to see where your labels are. Check out the documentation here.
Your labels have 0 width and height. You don't set their frame anywhere in this code, and the constraints which apply to them relate only to their position, no size can be determined from them. You either need to Specify their frame or add constraints from which it could be determined by layouting engine.

Resources