Why does UIStackView not stack a UILabel arrangedSubview? - ios

I have a UIStackView, and as arranged subviews, I have two UIViews, and a UILabel. The UIViews are stacked one after another, while the UILabel is aligned to the leading edge of the subview.
Code:
let stack = UIStackView()
stack.axis = .horizontal
stack.distribution = .fillProportionally
stack.alignment = .center
stack.spacing = 7
view.addSubview(stack)
stack.translatesAutoresizingMaskIntoConstraints = false
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let icon = UIView()
icon.backgroundColor = UIColor.red
stack.addArrangedSubview(icon)
icon.translatesAutoresizingMaskIntoConstraints = false
icon.heightAnchor.constraint(equalToConstant: 42).isActive = true
icon.widthAnchor.constraint(equalToConstant: 42).isActive = true
let icon2 = UIView()
icon2.backgroundColor = UIColor.red
stack.addArrangedSubview(icon2)
icon2.translatesAutoresizingMaskIntoConstraints = false
icon2.heightAnchor.constraint(equalToConstant: 42).isActive = true
icon2.widthAnchor.constraint(equalToConstant: 42).isActive = true
let label = UILabel()
label.lineBreakMode = .byWordWrapping
label.numberOfLines = 0
label.sizeToFit()
label.translatesAutoresizingMaskIntoConstraints = false
label.backgroundColor = .yellow
label.text = "Hello World! Again"
label.textColor = .black
stack.addArrangedSubview(label)
Output:

For clarity...
First, don't use .fillProportionally. Whatever you think that will do, it's wrong. You have explicitly set your two "icons" to be 42-pts wide each. If the stack view is using .fillProportionally, it will try to change the widths of those views, and you will get auto-layout conflicts.
Second, a UILabel with .numberOfLines = 0 must have a width. Otherwise, there is no way to know where to break the text... with a lot of text, it will extend way off the sides of the view.
Third, the line label.sizeToFit() isn't going to accomplish anything here. Just delete it.
If you add this line:
stack.widthAnchor.constraint(equalToConstant: 200.0).isActive = true
then the stack view will expand to 200-pts wide...
Auto-layout will give the first arranged view a width of 42 (because that's what you declared it to be), and the same for the second view. Since you've set the spacing to 7, it will then calculate
42 + 7 + 42 + 7
which equals 98. It subtracts that from the stack view's width:
200 - 98 = 102
and that is the width it will give your label.
Result:
If you don't want to explicitly set the width to 200, you can set it to a percentage of the superview's width, or give it leading and trailing constraints.

Try to change distribution to fill
stack.distribution = .fill
With multiline set
After commenting it

Related

Control the Size of TextField in UIKit Application

I am using the following to add a UITextField to a UIStackView. The main issue is the UITextField is expanding to take the complete height. What am I doing wrong? I want UITextField to be of 44 or 60 points height.
lazy var nameTextField: UITextField = {
let textfield = UITextField()
textfield.translatesAutoresizingMaskIntoConstraints = false
textfield.placeholder = "Budget name"
textfield.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 0))
textfield.leftViewMode = .always
textfield.borderStyle = .roundedRect
return textfield
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.green
setupUI()
}
private func setupUI() {
let stackView = UIStackView()
stackView.alignment = .leading
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = UIStackView.spacingUseSystem
stackView.isLayoutMarginsRelativeArrangement = true
stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20)
view.addSubview(stackView)
stackView.addArrangedSubview(nameTextField)
// add constraints on nameTextField
nameTextField.widthAnchor.constraint(equalToConstant: 200).isActive = true
nameTextField.heightAnchor.constraint(equalToConstant: 60).isActive = true
// add constraints stackview
stackView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
stackView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
}
}
A UIStackView arranges its subviews (.addArrangedSubview()).
So, you are telling auto-layout to:
make the text field 60-points tall
AND
make the text field as tall as the stack view
In this case, the stack view wins.
Edit - for clarification...
When you ran your app, you should have seen a bunch of auto-layout error / warning messages. That tells you that you have assigned conflicting constraints.
If you want the text field height to use the .heightAnchor.constraint(equalToConstant: 60) that you've assigned, you have a few options...
1 - Don't embed it in a stack view.
2 - Don't assign a height to the stack view (either directly or with top & bottom constraints).
3 - add additional arrangedSubviews to the stack view.
So, if you make only this change to your code:
stackView.addArrangedSubview(nameTextField)
// comment out this line
//stackView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
you'll get this:
If you leave that line in, and add a yellow-background UILabel as another arranged subview:
stackView.addArrangedSubview(nameTextField)
// leave this un-commented
stackView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
let label = UILabel()
label.text = "The Label"
label.backgroundColor = .yellow
label.widthAnchor.constraint(equalToConstant: 200).isActive = true
stackView.addArrangedSubview(label)
you'll get this:
because you gave the text field an explicit Height constraint, so the label height "stretches."
Or, if you add the label and omit the stack view's bottom anchor:
stackView.addArrangedSubview(nameTextField)
// comment out this line
//stackView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true
let label = UILabel()
label.text = "The Label"
label.backgroundColor = .yellow
label.widthAnchor.constraint(equalToConstant: 200).isActive = true
stackView.addArrangedSubview(label)
you'll get this:
because you gave the text field an explicit Height constraint, and let the label use its Intrinsic Content Size.

UIStackView alignment issue

I want to achieve this requirement
if vertical stack has two label than Text should be centre aligned to image
and if not than top aligned to Image
How can I achieve this without writing any code
You'll need to control the alignment of the outer (i.e final stack view which contains both the image and the labels' stack view) stack view.
As you will need to control which labels need to be added to the labels' stack view, I assume you will be doing this programmatically. So basically you'll need:
finalStackView.alignment = labelsStackView.arrangedSubviews.count > 2 ? .top : .center
Here is a complete example which produces the below outputs:
class ViewController: UIViewController {
let finalStackView = UIStackView()
override func viewDidLoad() {
super.viewDidLoad()
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.heightAnchor.constraint(equalToConstant: 200).isActive = true
imageView.widthAnchor.constraint(equalToConstant: 200).isActive = true
imageView.image = #imageLiteral(resourceName: "taylor-swift")
let label1 = UILabel()
let label2 = UILabel()
let label3 = UILabel()
let label4 = UILabel()
label1.translatesAutoresizingMaskIntoConstraints = false
label2.translatesAutoresizingMaskIntoConstraints = false
label3.translatesAutoresizingMaskIntoConstraints = false
label4.translatesAutoresizingMaskIntoConstraints = false
label1.text = "Hello"
label2.text = "72 mins"
label3.text = "Hello 3"
label4.text = "Hello 4"
let labelsStackView = UIStackView(arrangedSubviews: [label1, label2, label3, label4])
labelsStackView.translatesAutoresizingMaskIntoConstraints = false
labelsStackView.axis = .vertical
labelsStackView.distribution = .fill
labelsStackView.alignment = .leading
finalStackView.addArrangedSubview(imageView)
finalStackView.addArrangedSubview(labelsStackView)
finalStackView.axis = .horizontal
finalStackView.distribution = .fill
finalStackView.alignment = labelsStackView.arrangedSubviews.count > 2 ? .top : .center
view.addSubview(finalStackView)
finalStackView.translatesAutoresizingMaskIntoConstraints = false
finalStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
finalStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
finalStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}
}
With the exact code above the output is:
With 2 labels added to the labelsStackView, the output is:
Keep the labels vertical stackview in a Horizontal stackView.
if you have more than 2 labels, change Horizontal stackView alignment to top else keep it to center.
Your layout structure be like
> main stack view (Horizontal)
> Image
> stack view (Horizontal)
>labels stack view (Vertical)
> Labels

How to make a horizontal StackView to have the first element's width and fill the rest of it

I'm new with swift and trying to create an input field at the moment. My problem is, that I would like to have a Label as shown in the picture:
So far, I'm working with StackViews: One vertical one for the input fields, and three horizontal ones to have the Title and the user input. My code so far is as follows:
// Initialize outter stackview
let feedbackOutterSV = UIStackView()
view.addSubview(feedbackOutterSV)
feedbackOutterSV.translatesAutoresizingMaskIntoConstraints = false
feedbackOutterSV.axis = NSLayoutConstraint.Axis.vertical
NSLayoutConstraint.activate([
feedbackOutterSV.topAnchor.constraint(equalTo: tutorialText.bottomAnchor, constant: 10),
feedbackOutterSV.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
feedbackOutterSV.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
feedbackOutterSV.heightAnchor.constraint(equalToConstant: 300)
])
// Initalize inner stackview for title
let feedbackInnerSVTitle = UIStackView()
feedbackOutterSV.addArrangedSubview(feedbackInnerSVTitle)
feedbackInnerSVTitle.translatesAutoresizingMaskIntoConstraints = false
feedbackInnerSVTitle.axis = .horizontal
feedbackInnerSVTitle.alignment = .fill
feedbackInnerSVTitle.distribution = .fillProportionally
let titleLabel = UILabel()
feedbackInnerSVTitle.addArrangedSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.text = "feedback.input.title".localize()
titleLabel.font = UIFont.preferredFont(forTextStyle: .body)
titleLabel.textColor = .gray
let titleTextView = UITextView()
feedbackInnerSVTitle.addArrangedSubview(titleTextView)
titleTextView.translatesAutoresizingMaskIntoConstraints = false
titleTextView.font = UIFont.preferredFont(forTextStyle: .body)
titleTextView.isScrollEnabled = false
NSLayoutConstraint.activate([
titleLabel.widthAnchor.constraint(equalToConstant: 39)
])
This code gives the expected output for English, however I have to implement it in different languages, so I can't use a constant width.
Can anyone tell me how to change my code, so I don't need the constant constraint but the width of the Label is adjusted to the length of the word?
Thanks in advance
Couple things...
I assume you want the "title label" to be top-aligned with your textView, so change .fill to .top:
feedbackInnerSVTitle.alignment = .top // .fill
and, don't use .fillProportionally
feedbackInnerSVTitle.distribution = .fill // .fillProportionally
Now, you'll likely see each element taking 50% of the width, so change the content hugging priority for your title label:
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
and, finally, don't set a width constraint on your title label:
// NSLayoutConstraint.activate([
// titleLabel.widthAnchor.constraint(equalToConstant: 39)
// ])
Result:
In your code, width constraint on titleLabel must be set to titleLabel.intrinsicContentSize.width
NSLayoutConstraint.activate([
titleLabel.widthAnchor.constraint(equalToConstant: titleLabel.intrinsicContentSize.width)
])
Also, set the distribution of feedbackInnerSVTitle as .fill
feedbackInnerSVTitle.distribution = .fill
I think you could use NSLayoutConstraint.activate([
titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 0)] to let it grow depending on the content

Center stack view elements and not fill them

I am using a UIStackView as UITableView's BackGroundView property so when there was an error getting the collection that populates the tableView I can call a function that displays this stack view containing views that show a warning message and a retry button.
I tested doing a similar behaviour in an empty UIViewController so I could center the stackView and its children. The solution worked when I pinned the stack view to the superView's trailing and leading, centered it vertically and set it's top anchor to be greater or equal to the superView's top anchor and similarly it's bottom anchor is greater or equal to the superView's bottom anchor. I have also set the alignment to center and distribution to fill and all seemed to work properly.
Here are some screenshots:
I used this code in a UITableView's extension, but could only reproduce this behaviour. Are there any errors on this code?
func show(error: Bool, withMessage message : String? = nil, andRetryAction retry: (() -> Void)? = nil){
if error{
let iconLabel = UILabel()
iconLabel.GMDIcon = .gmdErrorOutline
iconLabel.textAlignment = .center
iconLabel.numberOfLines = 0
iconLabel.font = iconLabel.font.withSize(50)
iconLabel.textColor = Constants.Colors.ErrorColor
iconLabel.backgroundColor = .blue
let messageLabel = UILabel()
messageLabel.text = message ?? "Ocorreu um erro"
messageLabel.textColor = Constants.Colors.ErrorColor
messageLabel.numberOfLines = 0
messageLabel.textAlignment = .center
messageLabel.font = UIFont(name: "TrebuchetMS", size: 20)
messageLabel.backgroundColor = .green
var views: [UIView] = [iconLabel, messageLabel]
if let retry = retry{
let button = RaisedButton(title: "Tentar novamente")
button.pulseColor = Constants.Colors.PrimaryTextColor
button.backgroundColor = Constants.Colors.PrimaryColor
button.titleColor = .white
button.actionHandle(controlEvents: .touchUpInside, ForAction: retry)
button.contentEdgeInsets = UIEdgeInsetsMake(10,10,10,10)
views.append(button)
}
}else{
self.backgroundView = nil
}
}
let stack = UIStackView()
stack.spacing = 10
stack.axis = .vertical
stack.alignment = .center
stack.distribution = .fill
stack.translatesAutoresizingMaskIntoConstraints = false
for view in views{
view.translatesAutoresizingMaskIntoConstraints = false
stack.addArrangedSubview(view)
}
if self.tableFooterView == nil{
tableFooterView = UIView()
}
self.backgroundView = stack;
if #available(iOS 11, *) {
let guide = self.safeAreaLayoutGuide
stack.topAnchor.constraint(greaterThanOrEqualTo: guide.topAnchor).isActive = true
stack.bottomAnchor.constraint(greaterThanOrEqualTo: guide.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: guide.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: guide.trailingAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: guide.centerYAnchor).isActive = true
} else {
stack.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor).isActive = true
stack.bottomAnchor.constraint(greaterThanOrEqualTo: self.bottomAnchor).isActive = true
stack.leadingAnchor.constraint(equalTo: self.leadingAnchor).isActive = true
stack.trailingAnchor.constraint(equalTo: self.trailingAnchor).isActive = true
stack.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
}
A stack view knows its height if its elements all have an intrinsic size (ie its the sum of their individual heights + the inter item spacing). In this case because you have a 2 y position constraints, you are implying a height, so your constraints are unsatisfiable. The only y axis constraint you need is center vertically. get rid of the top and bottom constraints. The system will then use the intrinsic size to compute the height of the stack view and center it vertically in the background view. Leave your x axis constraints as is.

Programmatically changing size of 1 subview in UIStackView

I am currently making a calculator and want to change the size of my 0 button to the size of 2 subviews - which is half the size of the entire view. I want it to look exactly like apples calculator app, where the 0 is bigger than all the other buttons.
The way i layout my view is by having a vertical UIStackView and adding horizontal UIStackView's to it, just like the picture below.
Therefore, i want the last horizontal stack to have 3 arranged subviews but make the 0 button fill the exceeding space, so the , and = buttons are the same size as all other buttons.
Thank you.
Programmatically, you could set multiplier with constraint(equalTo:multiplier:), official docs: https://developer.apple.com/documentation/uikit/nslayoutdimension/1500951-constraint
So we could constraint the last two button with same width and make the first one two times longer than one of the other two.
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let btn1 = UIButton()
let btn2 = UIButton()
let btn3 = UIButton()
btn1.backgroundColor = .red
btn2.backgroundColor = .yellow
btn3.backgroundColor = .blue
let hStack = UIStackView(arrangedSubviews: [btn1, btn2, btn3])
hStack.axis = .horizontal
hStack.spacing = 1
view.addSubview(hStack)
hStack.translatesAutoresizingMaskIntoConstraints = false
hStack.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
hStack.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
hStack.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Here would be what you need:
btn2.widthAnchor.constraint(equalTo: btn3.widthAnchor).isActive = true
btn1.widthAnchor.constraint(equalTo: btn2.widthAnchor, multiplier: 2).isActive = true
}
You can use stackview.distribution = .fillEquallly for first 4 horizontal stackviews and use stackview.distribution = .fillPropotionally for the last horizontal stackview.
Then set with constraint for the 0 button to 50% of last horizontal stackview's width.

Resources