Stack View Distribution with Dynamic Label Width - ios

I have a UIStackView however, the first view of the subViews is a UILabel and it is not sizing it accordingly.
My code is as per below;
private let stackView: UIStackView = {
let view = UIStackView()
view.translatesAutoresizingMaskIntoConstraints = false
view.axis = .horizontal
view.distribution = .fillProportionally
view.alignment = .trailing
view.spacing = 8
return view
}()
and I, of course, add my subViews within init as below;
stackView.addArrangedSubview(currentBidLabel)
stackView.addArrangedSubview(seperator)
stackView.addArrangedSubview(adCreatedAtLabel)
stackView.addArrangedSubview(moreButton)
My current result is as per the illustration below;
The result I'm looking for is the following;
Red = Dynamic Label Width (same as Blue)
Green = Fixed Width
Blue = Fill Remaining Space
Cyan = Fixed Width

You should set the desired ContentHuggingPriority for each label. The following setup will be matched to your needs
redLabel.setContentHuggingPriority(. required, for: .horizontal)
greenLabel.setContentHuggingPriority(.required, for: .horizontal)
blueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
cyanLabel.setContentHuggingPriority(.required, for: .horizontal)
defaultLow means to fill the rest.
required means fit first.
And other values should be between these two.

Related

Vertical layout issue with 2 UILabels inside a StackView

I have a title label as defined below:
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 2
label.setContentCompressionResistancePriority(.init(rawValue: 999), for: .vertical)
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byTruncatingTail
return label
}()
titleLabel can grow and force to shrink the descriptionLabel.
The cell is height is fixed to 120 or 78. When the cell height is 78, the descriptionLabel height is 0
Both are added to a UIStackview
private lazy var labelStackView: UIStackView = {
let stackView = UIStackView()
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.axis = .vertical
stackView.distribution = .fill
return stackView
}()
The stackView is added inside the containerView with padding to the top, left, right & bottom. The containerView is pinned to the edges of the contentView.
The distribution on the stackView is set to fill to allow descriptionLabel to shrink. This set up causes the constraint to break.
Issue
titleLabel - Height is ambiguous for UILabel
descriptionLabel - Height and vertical position are ambiguous for UILabel
Is there a way to achieve this without breaking the constraint?
I tried setting content resistance priority for both labels as follows:
titleLabel -> label.setContentCompressionResistancePriority(.init(rawValue: 999), for: .vertical)
descriptionLabel -> label.setContentCompressionResistancePriority(.init(rawValue: 998), for: .vertical)
I still had no luck.
By setting a content hugging priority on the descriptionLabel, I was able to fix the layout ambiguity.
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byTruncatingTail
label.setContentHuggingPriority(.high, for: .vertical)
return label
}()

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.

UIStackView distribution and alignment of a multiline UILabel

I' struggling with some basic UIStackView distribution and alignment stuff.
I have an UICollectionViewCell which has a horizontal UIStackView at the contentView subview. This UIStackView has a vertical UIStackView for the three labels itself, and of course the UIImageView.
This is the code snippet for the screenshot below:
func createSubViews() {
// contains the UIStackview with the 3 labels and the UIImageView
containerStackView = UIStackView()
containerStackView.axis = .horizontal
containerStackView.distribution = .fill
containerStackView.alignment = .top
contentView.addSubview(containerStackView)
// the UIStackView for the labels
verticalStackView = UIStackView()
verticalStackView.axis = .vertical
verticalStackView.distribution = .fill
verticalStackView.spacing = 10.0
containerStackView.addArrangedSubview(verticalStackView)
categoryLabel = UILabel()
categoryLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
categoryLabel.textColor = UIColor.lightGray
verticalStackView.addArrangedSubview(categoryLabel)
titleLabel = UILabel()
titleLabel.numberOfLines = 3
titleLabel.lineBreakMode = .byWordWrapping
verticalStackView.addArrangedSubview(titleLabel)
timeLabel = UILabel()
timeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
timeLabel.textColor = UIColor.lightGray
verticalStackView.addArrangedSubview(timeLabel)
// UIImageView
imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 5
layer.masksToBounds = true
containerStackView.addArrangedSubview(imageView)
}
What I want to achive is, that the "time label" ("3 days ago") is always placed at the bottom of each UICollectionViewCell (aligned with the bottom of the UIImageView), regardless of the different title label lines.
I've played with various UIStackView distributions, constraining the "time label" and with the hugging priority of the "title label".
But anyhow I can't get it right. Any hints?
UPDATE
Since you're setting titleLabel.numberOfLines = 3, one way to do this is simply to append three newlines to the title text. That will force titleLabel to always consume its full height of three lines, forcing timeLabel to the bottom.
That is, when you set titleLabel.text, do it like this:
titleLabel.text = theTitle + "\n\n\n"
ORIGINAL
If you let one of the labels stretch vertically, the stretched label's text will be centered vertically within the stretched label's bounds, which is not what you want. So we can't let the labels stretch vertically. Therefore we need to introduce a padding view that can stretch but is otherwise invisible.
If the padding view gets squeezed down to zero height, the stack view will still put spacing before and after it, leading to double-spacing between titleLabel and timeLabel, which you also don't want.
So we'll need to implement all the spacing using padding views. Change verticalStackView.spacing to 0.
Add a generic UIView named padding1 to verticalStackView after categoryLabel, before titleLabel. Constrain its height to equal 10.
Add a generic UIView named padding2 to verticalStackView after titleLabel, before timeLabel. Constrain its height to greater than or equal to 10 so that it can stretch.
Set the vertical hugging priorities of categoryLabel, titleLabel, and timeLabel to required, so that they will not stretch vertically.
Constrain the height of verticalStackView to the height of containerStackView so that it will stretch one or more of its arranged subviews if needed to fill the vertical space available. The only arranged subview that can stretch is padding2, so it will stretch, keeping the title text near the top and the time text at the bottom.
Also, constrain your containerStackView to the bounds of contentView and set containerStackView.translatesAutoresizingMaskIntoConstraints = false.
Result:
Here's my playground:
import UIKit
import PlaygroundSupport
class MyCell: UICollectionViewCell {
var containerStackView: UIStackView!
var verticalStackView: UIStackView!
var categoryLabel: UILabel!
var titleLabel: UILabel!
var timeLabel: UILabel!
var imageView: UIImageView!
func createSubViews() {
// contains the UIStackview with the 3 labels and the UIImageView
containerStackView = UIStackView()
containerStackView.axis = .horizontal
containerStackView.distribution = .fill
containerStackView.alignment = .top
contentView.addSubview(containerStackView)
// the UIStackView for the labels
verticalStackView = UIStackView()
verticalStackView.axis = .vertical
verticalStackView.distribution = .fill
verticalStackView.spacing = 0
containerStackView.addArrangedSubview(verticalStackView)
categoryLabel = UILabel()
categoryLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
categoryLabel.textColor = UIColor.lightGray
verticalStackView.addArrangedSubview(categoryLabel)
let padding1 = UIView()
verticalStackView.addArrangedSubview(padding1)
titleLabel = UILabel()
titleLabel.numberOfLines = 3
titleLabel.lineBreakMode = .byWordWrapping
verticalStackView.addArrangedSubview(titleLabel)
let padding2 = UIView()
verticalStackView.addArrangedSubview(padding2)
timeLabel = UILabel()
timeLabel.font = UIFont.preferredFont(forTextStyle: .caption1)
timeLabel.textColor = UIColor.lightGray
verticalStackView.addArrangedSubview(timeLabel)
// UIImageView
imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.layer.cornerRadius = 5
layer.masksToBounds = true
containerStackView.addArrangedSubview(imageView)
categoryLabel.setContentHuggingPriority(.required, for: .vertical)
titleLabel.setContentHuggingPriority(.required, for: .vertical)
timeLabel.setContentHuggingPriority(.required, for: .vertical)
imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
containerStackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leadingAnchor.constraint(equalTo: containerStackView.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: containerStackView.trailingAnchor),
contentView.topAnchor.constraint(equalTo: containerStackView.topAnchor),
contentView.bottomAnchor.constraint(equalTo: containerStackView.bottomAnchor),
verticalStackView.heightAnchor.constraint(equalTo: containerStackView.heightAnchor),
padding1.heightAnchor.constraint(equalToConstant: 10),
padding2.heightAnchor.constraint(greaterThanOrEqualToConstant: 10),
])
}
}
let cell = MyCell(frame: CGRect(x: 0, y: 0, width: 320, height: 110))
cell.backgroundColor = .white
cell.createSubViews()
cell.categoryLabel.text = "MY CUSTOM LABEL"
cell.titleLabel.text = "This is my title"
cell.timeLabel.text = "3 days ago"
cell.imageView.image = UIGraphicsImageRenderer(size: CGSize(width: 110, height:110)).image { (context) in
UIColor.blue.set()
UIRectFill(.infinite)
}
PlaygroundPage.current.liveView = cell
The problem is the vertical stack view. You apparently want to say: the middle label's top should hug the MyCustomLabel bottom, but the 3 Days Ago bottom should hug the overall bottom. That is not something you can say to a stack view.
And even if that is not what you want to say, you would still need to make the vertical stack view take on the full height of the cell, and how are you going to do that? In the code you showed, you don't do that at all; in fact, your stack view has zero size based on that code, which will lead to all sorts of issues.
I would suggest, therefore, that you just get rid of all the stack views and just configure the layout directly. Your layout is an easy one to configure using autolayout constraints.

Why is image view stretching to fill half the width in a UIStackView?

I am taking an UIImageView on the left of the horizontal stack view and a label with number of lines set to 0. The issue is that both the UIImageView and the label are filling equal space even though I have set the stack view distribution property to fill. I don't want my image view to expand. Changing content hugging isn't helping.
Problem is: When telling the stack view to fill the content area it try to do it as best as possible. When setting the distribution type to fill it do not know how to fill. You have to provide this information by setting a width constraint via autolayout for the image. The label will fill the remaining area then.
Another way of doing this would be to add an invisible spacer object at the right side of the image + label stack.
private func spacer() -> UIView {
let stretchingView = UIView()
stretchingView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
stretchingView.backgroundColor = UIColor.clear
stretchingView.translatesAutoresizingMaskIntoConstraints = false
return stretchingView
}
private func labelWithImage() -> UIView {
let image = UIImage(systemName: "map")
let imageView = UIImageView(image: image)
let label = UILabel()
label.text = "3 Miles Away"
label.font = UIFont.systemFont(ofSize: 17, weight: .regular)
label.textColor = UIColor.gray
let stackView = UIStackView(arrangedSubviews: [imageView, label, spacer()])
stackView.alignment = .leading
stackView.axis = .horizontal
return stackView
}

UIStackview align left

I'm trying to align the items in UIStackview horizontally to the left.
Is there anyway to do this programmatically? I tried to do this via storyboard but was not successful. For some reason, the items in the UIStackView centers itself.
If you use StackView setting axis property to "Horizontal",
I think you can't align it to the left.
But there is a way to make it left visually.
set StackView leading constraint with "equal" relationship
set StackView trailing constraint with "greater than or equal"
like this
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
stackView.trailingAnchor.constraint(greaterThanOrEqualTo: view.trailingAnchor).isActive = true
Try Adding One Space View in Your Stack View , Like this
// To Make Our Buttons aligned to left We have added one spacer view
UIButton *spacerButton = [[UIButton alloc] init];
[spacerButton setContentHuggingPriority:UILayoutPriorityFittingSizeLevel forAxis:UILayoutConstraintAxisHorizontal];
[spacerButton setContentCompressionResistancePriority:UILayoutPriorityFittingSizeLevel forAxis:UILayoutConstraintAxisHorizontal];
[self.buttonsStackView addArrangedSubview:spacerButton];
And Make
self.buttonsStackView.distribution = UIStackViewDistributionFill;
Swift 4.2 + iOS 12.1 tested:
let stackView: UIStackView = UIStackView(frame: .zero)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .leading // <- aligns each of your items to the left.
// further setup + setup constraints
// ...
Thanks to #Ankit Maurya, solution with example for swift
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
let spacer = UIView()
spacer.isUserInteractionEnabled = false
spacer.setContentHuggingPriority(.fittingSizeLevel, for: .horizontal)
spacer.setContentCompressionResistancePriority(.fittingSizeLevel, for: .horizontal)
// stackView.addArrangedSubview.... any u need
stackView.addArrangedSubview(spacer)
Example:
Use the UIStackView.alignment property.
Reference: https://developer.apple.com/reference/uikit/uistackview/1616243-alignment
Thanks for the advice guys. I tried playing with the options on UIStackView.alignment but it never really worked as desired. In the end I messed with the width of the UIStackView to achieve the desired results.
if(prdDTO.getShellIcn() == "Y")
{
let icon = UIImageView()
icon.contentMode = UIViewContentMode.scaleAspectFit
icon.image = #imageLiteral(resourceName: "Shellfish")
prdIngredients.addArrangedSubview(icon)
stackviewWidth += 25
}
if(prdDTO.getVegeIcn() == "Y")
{
let icon = UIImageView()
icon.contentMode = UIViewContentMode.scaleAspectFit
icon.image = #imageLiteral(resourceName: "Vege")
prdIngredients.addArrangedSubview(icon)
stackviewWidth += 25
}
if(prdDTO.getSpicyIcn() == "Y")
{
let icon = UIImageView()
icon.contentMode = UIViewContentMode.scaleAspectFit
icon.image = #imageLiteral(resourceName: "Spicy")
prdIngredients.addArrangedSubview(icon)
stackviewWidth += 25
}
prdIngredients.widthAnchor.constraint(equalToConstant: CGFloat(stackviewWidth))
Remove the trailing constrant (even though XCode complains!!!) and your problem will be solved. This will cause the the view to expand as you add items to it.
Storyboard
Click Attributes Inspector
Select Trailing from the Drop Down menu when Alignment is selected.
Programmatically
Change the UIStackView.alignment property when initializing.

Resources