Swift 3 UILabel get actual height (intrinsicContentSize) - ios

I'm trying to simulate a kind of "Please wait..." UILabel. The label's text must be regularly updated. So far everything works as expected. However, I need to get the intrinsic content height of the label to be able to position its container view (UIView).
The label is the one with the red background, whereas the one with the white background is its container.
I've tried a few different approaches, unfortunately, all in vain. Any help would be greatly appreciated.
private func createBusyLabel(labelText: String) -> CGFloat {
self.busyViewContainer.addSubview(self.busyLabel)
self.busyLabel.backgroundColor = UIColor.red
self.busyLabel.numberOfLines = 0
self.busyLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
self.busyLabel.sizeToFit()
//set the constraints, but skip height constraints
self.busyLabel.translatesAutoresizingMaskIntoConstraints = false
self.busyLabel.horizontalLeft(toItem: self.busyViewContainer, constant: 60)
self.busyLabel.horizontalRight(toItem: self.busyViewContainer, constant: -10)
self.busyLabel.topConstraints(toItem: self.busyViewContainer, constant: 10)
self.busyLabel.text = labelText
//calculate height with margin
let height: CGFloat = self.busyLabel.intrinsicContentSize.height + 20
return height
}
Also, the line counting function, from a previously asked and already answered question, delivers only 1
Here is what it look like after I set the bottom constraint:

A million Thanks to ozgur, who changed my approach. Ozgur, your code works perfect, but unfortunately not for me, as I faced problems with bottomLayoutGuide part. The reason for this is that the label and its container are created in an external class.
Previously I tried to set bottom constraint to the label, which did not return the expected result. However, inspired by ozgur's answer, this time I simply set the bottom constraint to its container and not the label, giving in expected result, like following:
self.busyViewContainer.bottomConstraints(toItem: self.busyLabel, constant: 10)
Thanks to all who put in their precious efforts.
private func createBusyLabel(labelText: String) -> Void {
self.busyLabel.text = labelText
self.busyLabel.font = UIFont.getGlobalFont(size: _textSizeSmall, type: "bold")
self.busyLabel.backgroundColor = UIColor.red
// handle multiline problem
self.busyLabel.numberOfLines = 0
self.busyLabel.lineBreakMode = NSLineBreakMode.byWordWrapping
self.busyLabel.sizeToFit()
self.busyViewContainer.addSubview(self.busyLabel)
self.busyLabel.translatesAutoresizingMaskIntoConstraints = false
self.busyLabel.horizontalLeft(toItem: self.busyViewContainer, constant: 60)
self.busyLabel.horizontalRight(toItem: self.busyViewContainer, constant: -10)
self.busyLabel.topConstraints(toItem: self.busyViewContainer, constant: 10)
// the following line made the difference
self.busyViewContainer.bottomConstraints(toItem: self.busyLabel, constant: 10)
}

The simple fix is adding the bottom constraint between 'self.busyViewContainer' and its superview.
Following your code and syntax it can be something like this:
self.busyLabel.bottomConstraints(toItem: self.busyViewContainer, constant: 10)
It is a common problem with 'Unsatisfiable Constraints'. The autolayout should ensure it satisfies horizontal axis layout so it needs to have both top and bottom constraints in this case.
Apple doc - Unsatisfiable Constraints
Apple doc - Logical Errors
UPD: In this case, the layout of the superview can define a height with intrinsicContentSize:
var intrinsicContentSize: CGSize { return ... }
Where the height of the parent view will be calculated based on the label one.

Related

StackView constraints with UIViews

I'm trying to setup a stackview in the middle of the screen with a padding of 20 to the left an right. Inside, I want to place two custom UIViews, but I don't quit understand how to do it. I tried giving the UIViews their respective Height's and Width's but I got nothing.
I believe the stack view has all the correct constraints. Here's the code:
func setupTeamViews() {
view.addSubview(teamsStackView)
teamsStackView.distribution = .fill
teamsStackView.axis = .vertical
teamsStackView.spacing = 20
teamsStackView.alignment = .fill
NSLayoutConstraint.activate([
teamsStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
teamsStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
teamsStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
teamsStackView.heightAnchor.constraint(equalToConstant: 420)
])
let height = CGFloat((teamsStackView.frame.height / 2) - 20)
let width = CGFloat((teamsStackView.frame.width - 20))
firstTeamView = ATTeamView(width: width, height: height)
firstTeamView.changeColor(color: .lightBlue)
firstTeamView.setTeamName(name: "Tobias")
firstTeamView.setNewPoints(points: "0")
secondTeamView = ATTeamView(width: width, height: height)
secondTeamView.changeColor(color: .white)
secondTeamView.setTeamName(name: "Valen")
secondTeamView.setNewPoints(points: "0")
teamsStackView.addArrangedSubview(firstTeamView)
teamsStackView.addArrangedSubview(secondTeamView)
}
How does a stackview work with UIview's? As far I understand, UIViews don't have intrinsicContentSize, but I don't know how to deal with that.
In stead of doing it programmatically you can add two container views attach them to your view controller, and then active them with the alpha.
This will make constraints easier and you can still program anything extra you would like
myView1.alpha = 1 // activate
myView2.alpha = 0 // deactivate
My code wasn't working because I've forgotten to set teamsStackView.translatesAutoresizingMaskIntoConstraints = false

Subview (UILabel) doesn't respect the constraints when placed in superview (UITextView)

I tried my own "placeholder in a UITextView" implementation.
My approach was this:
I create a UILabel in a UITextView subclass, and I set the constraints of the UILabel to match the size of its superview (UITextView).
This is the code where I create the UILabel and assign it to a class variable named placeholderLabel in awakeFromNib():
placeholderLabel = UILabel()
placeholderLabel.text = placeholder
placeholderLabel.numberOfLines = 0
placeholderLabel.lineBreakMode = .byWordWrapping
placeholderLabel.textAlignment = .left
The following code is where I add the UILabel as a subview, and I set the constraints, again in awakeFromNib():
placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
placeholderLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: textContainerInset.left + 4).isActive = true
placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: textContainerInset.top).isActive = true
placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: textContainerInset.right + 4).isActive = true
placeholderLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: textContainerInset.bottom).isActive = true
I also have a property where I set the placeholder's text, where I have a didSet observer, which sets placeholderLabel's text and then calls layoutIfNeeded() in order to have the constraints recalculated in case the UILabel extends to a second (or third, etc) row:
var placeholder: String = "" {
didSet {
placeholderLabel.text = placeholder
layoutIfNeeded()
}
}
The issue is that I have the following result:
The UILabel extends beyond it's superviews bounds (to the right), and it appears that it doesn't respect the constraints. I run the visual debugger which confirmed the same thing:
It seems that there is a width constraint which follows the UILabel's content width instead of following the constraint I have set in place (in this case it creates a width of 431 whereas the superview's width is 288).
Is there something that I miss?
First of all you have to use a negative value for the right constraint's constant (or - to use a positive value - switch the items placeholderLabel.rightAnchor / rightAnchor).
The real problem though is the fact that UITextView is a subclass of UIScrollView. In your case adding the UILabel with a large text as a subview and constraining its edges to the textview's edges results in the textview's contentSize to grow. The textview becomes horizontally scrollable.
Printing out the textview's contentSize before and after adding the label results in different values for the width (before: 335.0, after: 505.0).
Proof: https://www.dropbox.com/s/eogvl2c5r76c6cl/example.mov?dl=0
You could work around that problem by not creating the right but a width constraint instead:
// placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -(textContainerInset.right + 4)).isActive = true
placeholderLabel.widthAnchor.constraint(equalTo: widthAnchor, constant: -(textContainerInset.left + 4 + textContainerInset.right + 4)).isActive = true
with right should be minus
placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: - textContainerInset.right - 4).isActive = true

Stack view - but with "proportional" gaps

Imagine a stack view with four items, filling something. (Say, filling the screen).
Notice there are three gaps, ABC.
(Note - the yellow blocks are always some fixed height each.)
(Only the gaps change, depending on the overall height available to the stack view.)
Say UISV is able to draw everything, with say 300 left over. The three gaps will be 100 each.
In the example, 9 is left over, so A B and C are 3 each.
However.
Very often, you want the gaps themselves to enjoy a proportional relationship.
Thus - your designer may say something like
If the screen is too tall, expand the spaces at A, B and C. However. Always expand B let's say 4x as fast as the gaps at A and B."
So, if "12" is left over, that would be 2,8,2. Whereas when 18 is left over, that would be 3,12,3.
Is this concept available in stack view? Else, how would you do it?
(Note that recently added to stack view, you can indeed specify the gaps individually. So, it would be possible to do it "manually", but it would be a real mess, you'd be working against the solver a lot.)
You can achieve that by following workaround. Instead of spacing, for each space add a new UIView() that would be a stretchable space. And then just add constraints between heights of these "spaces" that would constrain their heights together based on the multipliers you want, so e.g.:
space1.heightAnchor.constraint(equalTo: space2.heightAnchor, multiplier: 2).isActive = true
And to make it work I think you'd have to add one constraint that would try to stretch those spaces in case there is free space:
let stretchingConstraint = space1.heightAnchor.constraint(equalToConstant: 1000)
// lowest priority to make sure it wont override any of the rest of constraints and compression resistances
stretchingConstraint.priority = UILayoutPriority(rawValue: 1)
stretchingConstraint.isActive = true
The "normal" content views would have to have intrinsic size or explicit constraints setting their heights to work properly.
Here is an example:
class MyViewController: UIViewController {
fileprivate let stack = UIStackView()
fileprivate let views = [UIView(), UIView(), UIView(), UIView()]
fileprivate let spaces = [UIView(), UIView(), UIView()]
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(stack)
// let stack fill the whole view
stack.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: self.view.topAnchor),
stack.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
stack.leftAnchor.constraint(equalTo: self.view.leftAnchor),
stack.rightAnchor.constraint(equalTo: self.view.rightAnchor),
])
stack.alignment = .fill
// distribution must be .fill
stack.distribution = .fill
stack.spacing = 0
stack.axis = .vertical
for (index, view) in views.enumerated() {
stack.addArrangedSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false
// give it explicit height (or use intrinsic height)
view.heightAnchor.constraint(equalToConstant: 50).isActive = true
view.backgroundColor = .orange
// intertwin it with spaces
if index < spaces.count {
stack.addArrangedSubview(spaces[index])
spaces[index].translatesAutoresizingMaskIntoConstraints = false
}
}
// constraints for 1 4 1 proportions
NSLayoutConstraint.activate([
spaces[1].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 4),
spaces[2].heightAnchor.constraint(equalTo: spaces[0].heightAnchor, multiplier: 1),
])
let stretchConstraint = spaces[0].heightAnchor.constraint(equalToConstant: 1000)
stretchConstraint.priority = UILayoutPriority(rawValue: 1)
stretchConstraint.isActive = true
}
}
Remarkably, #MilanNosáľ 's solution works perfectly.
You do not need to set any priorities/etc - it works perfectly "naturally" in the iOS solver!
Set the four content areas simply to 50 fixed height. (Use any intrinsic content items.)
Simply don't set the height at all of "gap1".
Set gap2 and gap3 to be equal height of gap1.
Simply - set the ratios you want for gap2 and gap3 !
Versus gap1.
So, gap2 is 0.512 the height of gap1, gap3 is 0.398 the height of gap1, etc.
It does solve it in all cases.
Fantastic!!!!!!!!!!
So: in the three examples (being phones with three different screen heights). In fact the relative heights of the gaps, is always the same. Your design department will rejoice! :)
Created: a gist with a storyboard example
The key here is Equal Heights between your arranged views and your reference view:
And then change the 'Multiplier` to your desired sizes:
In this example I have 0.2 for the main view sizes (dark grey), 0.05 within the pairs (black), and 0.1 between the pairs (light grey)
Then simply changing the size of the containing view will cause the views to re-size proportionally:
This is entirely within the storyboard, but you could do the same thing in code.
Note that I'm using only proportions within the StackView to avoid having an incorrect total size, (and making sure they add up to 1.0), but it should be possible to also have some set heights within the StackView if done correctly.

How do i make the constraints resize the buttons correctly?

I've added a stoplight image and red, yellow, and green buttons. I want to have the buttons resize to iPhone 4S and iPhone 6S screens, but the buttons either disappear off the page or are the wrong size for the iPhone 4S. I thought the number of point would resize proportionately, but it appears it does not. Any help would be appreciated, I really want to understand constraints but I am just not getting it! Normally I would just do a x-position/screensize, y-position/screensize to relocated it, but this could be noticeably too long.
Here is the constraints of the latest incorrect location. When I try to select the stoplight image, it won't provide a constraint for the leading and trailing edge to the stoplight image.
The yellow button is placed against the stoplight image, but it won't resize.
The easiest solution would be to give all images fixed values for their width and height constraints. Then you can align the spotlightImage in the superview as you wish and define the alignment of the circle images relative to the stoplight image.
However, if you would like to stretch the width of the stoplight image depending on the width of the screen, this is a complex problem. I played around a bit trying to define all constraints in storyboard, but could not come up with a proper solution. What one ideally would like to do, for example, is define the centreX of the circles proportionally to the spotlight image's width. Similarly for the y position. Unfortunately this is not possible.
In code one have a little bit more control. Here is a solution that will work. It is not pretty, because you are actually recalculating the width of the spotlightImage, but it works :-)
class ViewController: UIViewController {
lazy var stopLightImageView: UIImageView = {
return UIImageView(image: UIImage(named:"stopLight"))
}()
lazy var circleImageView: UIImageView = {
return UIImageView(image: UIImage(named:"circle"))
}()
override func viewDidLoad() {
super.viewDidLoad()
setupViews()
}
private func setupViews() {
//Values at start. This is used to calculate the proportional values, since you know they produce the correct results.
let stoplightStartWidth: CGFloat = 540
let stoplightStartHeight: CGFloat = 542
let circleStartWidth: CGFloat = 151
let circleStartLeading: CGFloat = 231
let circleStartTop: CGFloat = 52
let screenWidth = UIScreen.mainScreen().bounds.size.width
let stoplightMargin: CGFloat = 20
self.view.addSubview(stopLightImageView)
stopLightImageView.translatesAutoresizingMaskIntoConstraints = false
//stoplightImage constraints
stopLightImageView.leadingAnchor.constraintEqualToAnchor(self.view.leadingAnchor, constant: stoplightMargin).active = true
stopLightImageView.trailingAnchor.constraintEqualToAnchor(self.view.trailingAnchor, constant: -stoplightMargin).active = true
stopLightImageView.centerYAnchor.constraintEqualToAnchor(self.view.centerYAnchor, constant: 0).active = true
stopLightImageView.heightAnchor.constraintEqualToAnchor(stopLightImageView.widthAnchor, multiplier: stoplightStartWidth/stoplightStartHeight).active = true
self.view.addSubview(circleImageView)
circleImageView.translatesAutoresizingMaskIntoConstraints = false
//circle constraints
circleImageView.widthAnchor.constraintEqualToAnchor(stopLightImageView.widthAnchor, multiplier: circleStartWidth/stoplightStartWidth).active = true
circleImageView.heightAnchor.constraintEqualToAnchor(circleImageView.widthAnchor, multiplier: 1).active = true
let stoplightWidth = screenWidth - 2*stoplightMargin
let stoplightHeight = stoplightWidth * stoplightStartHeight/stoplightStartWidth
circleImageView.leadingAnchor.constraintEqualToAnchor(stopLightImageView.leadingAnchor, constant: stoplightWidth*circleStartLeading/stoplightStartWidth).active = true
circleImageView.topAnchor.constraintEqualToAnchor(stopLightImageView.topAnchor, constant: stoplightHeight*circleStartTop/stoplightStartHeight).active = true
}
}
Constraints are tricky, and it looks like you have a lot going on there. It's hard to tell you exactly what to do for this so, here's what I would try to do if I was having this issue(hopefully one works for you):
Set the images in the Attributes Inspector to either Aspect Fit or Redraw... That should fix your issue with them being different shapes.
Also look through the list of constraints to see if one relies on another, (for example the red and yellow seem to have similar constraints). If they rely on each other, ensure to satisfy any constraints that aren't yet - based off of the "parent" image.
Select everything and set to "Reset to Suggested Constraints". Build and run. If that doesn't fix it then there's only a few things left you can do.
Remove all the constraints on every object. Start with the black image and add missing constraints... or set it to "Center Horizontally in Container". Right click and drag the image or asset to your "view" or to the yellow "First" circle located above.
Hopefully this helps.

preferredMaxLayoutWidth not working in Swift?

I have tried to make a UILabel that is a certain width using preferredMaxLayoutWidth but no matter what I do it won't work. Can you help me? I have tries so many different combinations to make it work.
#IBAction func addBottomTextButton(sender: AnyObject) {
if addBottomTextField.text.isEmpty == false {
let halfScreenWidth = screenSize.width * 0.5
let bottomScreenPosition = screenSize.width
memeBottomText = addBottomTextField.text
fontName = "Impact"
let memeBottomTextCaps = memeBottomText.uppercaseString // --> THIS IS A STRING!
labelBottom.text = memeBottomTextCaps
labelBottom.textColor = UIColor.blackColor()
labelBottom.textAlignment = .Center
labelBottom.font = UIFont(name: fontName, size: 32.0)
labelBottom.sizeToFit()
labelBottom.userInteractionEnabled = true
labelBottom.adjustsFontSizeToFitWidth = true
labelBottom.numberOfLines = 1
labelBottom.preferredMaxLayoutWidth = screenSize.width
labelBottom.setTranslatesAutoresizingMaskIntoConstraints(true)
var r = CGFloat(halfScreenWidth)
var s = CGFloat(bottomScreenPosition)
labelBottom.center = CGPoint(x: r, y: s)
self.view.addSubview(labelBottom)
self.view.addConstraint(NSLayoutConstraint(item: labelBottom, attribute:
NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: labelBottom,
attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0))
dismissKeyboard()
}
}
Judging by your code I'd say your problem was you haven't got your constraints setup correctly and you're mixing using NSLayoutConstraints with setting the position using center and setting the size using sizeToFit.
Firstly, in the constraint you've setup you're relating labelBottom (the item argument) to itself (the toItem argument). I'm not exactly sure what you were trying to achieve with that? I'd recommend having a look at some tutorials on AutoLayout if you're unfamiliar with its concepts. Here's a good one: http://www.raywenderlich.com/50317/beginning-auto-layout-tutorial-in-ios-7-part-1
Secondly, just a small point, on the line let memeBottomTextCaps = memeBottomText.uppercaseString you've written // --> THIS IS A STRING. An easier way to remind yourself of the variable type when looking back at your code could be to use: let memeBottomTextCaps: String = memeBottomText.uppercaseString.
Thirdly, preferredMaxLayoutWidth isn't used to set the width of a UILabel - that's what the frame is for (or NSLayoutConstraints if you're using AutoLayout).
Lets get on with it!
Here's an example of how to create a label that is pinned to the bottom edge of its container view and is not allowed to be wider than it's container: (Keep in mind that all this can be done in IB)
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel()
// 1.
label.setTranslatesAutoresizingMaskIntoConstraints(false)
// 2.
label.text = // Put your text here.
// 3.
self.view.addSubview(label)
// 4.
let pinToBottomConstraint = NSLayoutConstraint(item: label,
attribute: .Bottom,
relatedBy: .Equal,
toItem: self.view,
attribute: .Bottom,
multiplier: 1.0,
constant: -8.0)
// 5.
let horizontalConstraints = NSLayoutConstraint.constraintsWithVisualFormat("|-8-[label]-8-|",
options: .DirectionLeadingToTrailing,
metrics: nil,
views: ["label" : label])
// 6.
self.view.addConstraint(pinToBottomConstraint)
self.view.addConstraints(horizontalConstraints)
}
}
The following referrers to the commented numbers in the code above.
1. You need to set setTranslatesAutoresizingMaskIntoConstraints to false to stop constraints being created that would otherwise conflict with the constraints we're going to create later. Here's the what Apple have to say about it:
Because the autoresizing mask naturally gives rise to constraints that fully specify a view’s position, any view that you wish to apply more flexible constraints to must be set to ignore its autoresizing mask using this method. You should call this method yourself for programmatically created views. Views created using a tool that allows setting constraints should have this set already.
2. You need to make sure you put your own text here, otherwise the code won't run.
3. The label must be added to the view hierarchy before adding constraints between it and it's superview! Otherwise, in this case, you'll get a runtime error saying:
Unable to parse constraint format:
Unable to interpret '|' character, because the related view doesn't have a superview
|-8-[label]-8-|
This is due to our horizontalConstraints needing to know the label's superview (the superview is denoted by the "|") but the label doesn't have a superview.
4. The pinToBottomConstraint constraint does what it says. The constant of -8 just specifies that I want the label to be 8 points from the bottom of its container view.
We don't need to create a constraint to specify the label's size - that's an intrinsic property of the UILabel which is determined, for example, by the number of lines and font.
5. The horiontalConstraints are created using Visual Format Language. Here's a good tutorial: http://code.tutsplus.com/tutorials/introduction-to-the-visual-format-language--cms-22715 Basically, "|-8-[label]-8-|" creates constraints to pin the left and right edges of the label to the left and right edges of its superview.
6. Finally add the constraints!
This is what it looks like:
I hope that answers your question.
I think the property only work for multiline situation.
// Support for constraint-based layout (auto layout)
// If nonzero, this is used when determining -intrinsicContentSize for multiline labels
#available(iOS 6.0, *)
open var preferredMaxLayoutWidth: CGFloat
And it indeed true after my test. So, we need set multiline. It will work.
titleLabel.numberOfLines = 0
I don't why Apple limit it to only multiline. In fact, we often need to set max width on label easily by one property.
Finally, if we want set max width , we need set max constaint, like the following
if device.isNew == "1" {
self.title.mas_updateConstraints { (make) in
make?.width.lessThanOrEqualTo()(163.w)
}
self.newTag.isHidden = false
} else {
self.newTag.isHidden = true
self.title.mas_updateConstraints { (make) in
make?.width.lessThanOrEqualTo()(207.w)
}
}

Resources