UIStackView - Three arranged subviews with relative widths - ios

I have an outer UIStackView, call it outerStackView with the following properties:
outerStackView.axis = .Horizontal
outerStackView.distribution = .Fill
outerStackView.spacing = 10
The outerStackView has three arranged subviews (think of them as 3 columns) which also happen to be UIStackViews.
I want to set the width of the three arranged subviews (columns) to be a relative multiplier of the outerStackView's width.
So I tried doing this:
leftColumnStackView = UIStackView()
middleColumnStackView = UIStackView()
rightColumnStackView = UIStackView()
outerStackView.addArrangedSubview(leftColumnStackView)
outerStackView.addArrangedSubview(middleColumnStackView)
outerStackView.addArrangedSubview(rightColumnStackView)
leftColumnStackView.widthAnchor.constraintEqualToAnchor(outerStackView.widthAnchor, multiplier: 0.4, constant: 0).active = true
middleColumnStackView.widthAnchor.constraintEqualToAnchor(outerStackView.widthAnchor, multiplier: 0.4, constant: 0).active = true
rightColumnStackView.widthAnchor.constraintEqualToAnchor(outerStackView.widthAnchor, multiplier: 0.2, constant: 0).active = true
This throws a bunch of autolayout errors.
How would I make these three column UIStackViews have a relative width to their parent view?

I solved this by giving the left and middle columns relative widths (taking into consideration the spacing between them), and the right column simply filled in the space.
leftColumnStackView.widthAnchor.constraintEqualToAnchor(outerStackView.widthAnchor, multiplier: 0.38, constant: -20).active = true
middleColumnStackView.widthAnchor.constraintEqualToAnchor(outerStackView.widthAnchor, multiplier: 0.38, constant: -20).active = true

Related

IOS SafeAreaLayoutGuide anchor for landscape screen

I've been following Paul Hudsons' Hacking with Swift tutorials and I'm up to project 6 where he uses layout constraint programmatically. I've been doing this kind of task solely using Interface Builder, but I'm keen to learn on how to do it programmatically.
From his tutorial, we have the following code that add 5 UILabels to the main controller's view.
let label1 = UILabel()
label1.translatesAutoresizingMaskIntoConstraints = false
label1.text = "THESE"
label1.backgroundColor = #colorLiteral(red: 0.5725490451, green: 0, blue: 0.2313725501, alpha: 1)
label1.sizeToFit()
// do the same with label2, label3, label4, label5
view.addSubview(label1)
view.addSubview(label2)
view.addSubview(label3)
view.addSubview(label4)
view.addSubview(label5)
and then I can add constraints manually:
let dictionary = [
"label1": label1,
"label2": label2,
"label3": label3,
"label4": label4,
"label5": label5
]
let metrics=[ "labelHeight":80]
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[label1(labelHeight#999)]-[label2(label1)]-[label3(label1)]-[label4(label1)]-[label5(label1)]-(>=10)-|", options: [], metrics: metrics, views: dictionary))
for label in dictionary.keys {
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[\(label)]|", options: [], metrics:nil, views: dictionary))
}
as you can see, I'm setting the first label's height to be 80. Then set label1 to have priority of 999, and make the remaining labels to follow label1's height constraint.
This is working fine, both in portrait and landscape mode.
Now i'm converting the code to use anchor.
let heightConstraint = label1.heightAnchor.constraint(equalToConstant: 88)
heightConstraint.priority = UILayoutPriority(rawValue: 999)
heightConstraint.isActive = true
for label in [label2, label3, label4, label5] {
label.heightAnchor.constraint(equalTo: label1.heightAnchor, multiplier: 1).isActive = true
}
var previousLabel : UILabel?
for label in [label1, label2, label3, label4, label5] {
label.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
label.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
if let previousLabel = previousLabel {
label.topAnchor.constraint(equalTo: previousLabel.bottomAnchor, constant: 10).isActive = true
} else {
label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
}
previousLabel = label
}
label5.bottomAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 10.0).isActive = true
I think I'm missing something here, because
when the app is in portrait mode, it is trying to fill the entire screen
when the app is in landscape mode, label5 is chopped off.
I think i'm missing something here when using anchor? I'm guessing it is this bit:
-(>=10)-
But i'm not sure how to do it with anchor mode. Any assistance would be greatly appreciated!
For label5 you should change a bottom constraint to:
label5.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10.0).isActive = true
It should be a negative number since you fix it to the anchor, which is below (in contrast to previous anchors where you fix it to the label above your current one). And for negative numbers you need to use lessThanOrEqualTo.
For the vertical layout it works fine too since constraint is lessThanOrEqualTo:
This question is pretty old but I am also following too Paul Hudsons' Hacking with Swift tutorials and I have difficulties in Project 6 about same issue. But I figured out and it might be helpful to any other person.
For trailing and bottom constraints, value should be negative both view.safeAreaLayoutGuide.bottomAnchor and view.trailingAnchor. As far as I understand, dimension is determined always to rightward and downward.
label5.bottomAnchor.constraint(greaterThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: 10.0).isActive = true
With this code, chopping off is pretty normal because by entering a positive value (+10.0), you already initialize the contsraint in the way that your label5's bottomAnchor will be under the safeAreaLayoutGuide bottomAnchor's by 10 units.
So, why does label5 seem in the view, entirely? Because label1 is attached to safeAreaLayoutGuide, your labels have determined vertical space between each other. Probably when you run the code, XCode will show some error and ignore some constraint. You may try to not determine a specific height. You can use:
label1.heightAnchor.constraint(greaterThanOrEqualToConstant: 80).isActive = true
And for the safeAreaLayoutGuide:
label5.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -10.0).isActive = true

ios Swift tableView dynamic cells constraints for labels and image programatically

I have labels and an image. I want labels above image. And image without leading and trailing margin constraints.
I have tried the following constraints but can't seem to get it right. And the row height for the cells doesn't adjust accordingly in landscape mode.
addSubview(videolabel1)
videolabel1.translatesAutoresizingMaskIntoConstraints = false
videolabel1.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
videolabel1.trailingAnchor.constraint(equalTo: trailingAnchor
, constant: 20).isActive = true
videolabel1.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true
videolabel1.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 20).isActive = true
addSubview(image)
image.translatesAutoresizingMaskIntoConstraints = false
image.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
image.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
image.heightAnchor.constraint(equalToConstant: 150).isActive = true
image.widthAnchor.constraint(equalTo: videoimage.heightAnchor
, multiplier: 16/9).isActive = true
image.topAnchor.constraint(equalTo: videodate.bottomAnchor, constant: 12).isActive = true
image.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 12).isActive = true
Try using UIStackView's when you want to arrange views in a horizontal or vertical fashion. The UIStackView can take care of most constraints for you.
You can try adding a parent UIStackView with vertical orientation, that contains another UIStackView with horizontal orientation (this UIStackView will contain two UILabel's), followed by the UIImageView, and another UILabel.
parentStackView = UIStackView()
parentStackView.axis = .vertical
parentStackView.distribution = .equalSpacing
parentStackView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(parentStackView)
parentStackView.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
parentStackView.widthAnchor.constraint(equalTo: self.contentView.widthAnchor, multiplier: 1).isActive = true
parentStackView.heightAnchor.constraint(equalTo: self.contentView.heightAnchor, multiplier: 1).isActive = true
parentStackView.isLayoutMarginsRelativeArrangement = true
parentStackView.addArrangedSubview(stackView)
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.spacing = 10
stackView.addArrangedSubview(label)
label.text = "First Label";
stackView.addArrangedSubview(label2)
label2.text = "Second Label";
parentStackView.addArrangedSubview(imageView);
imageView.image = UIImage(named: "imageview");
NSLayoutConstraint.activate([imageView.heightAnchor.constraint(equalToConstant:UIScreen.main.bounds.width / 2) ]);
imageView.contentMode = .scaleAspectFill;
imageView.clipsToBounds = true;
parentStackView.addArrangedSubview(label3)
label3.text = "Vertical Label below Image";
The result will look something like:
Set bottom and top anchor according to sequential view on top and bottom. Here, you are setting bottom anchor of both videolabel1 and videodate to bottom anchor which is wrong:-
set videoLabel trailing and top anchor and remove bottom anchor:-
videolabel1.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20).isActive = true
videolabel1.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true
set videoLabel trailing anchor as below and remove bottom anchor:-
videodate.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20).isActive = true
Also, remove bottom anchor of imagview and set bottom anchor constant as shown below:-
videotitle.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12).isActive = true

Programmatic constraints cut the bottom of my label off

I have a Nib file with a root UITableViewCell and child UILabel that I anchor at run time using programmatic constraints
lblAccountItemTitle.translatesAutoresizingMaskIntoConstraints = false
lblAccountItemTitle.topAnchor.constraint(lessThanOrEqualTo: self.topAnchor, constant: 16).isActive = true
lblAccountItemTitle.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -16).isActive = true
lblAccountItemTitle.leadingAnchor.constraint(equalTo: imgAccountItemLeft.trailingAnchor, constant: 16).isActive = true
lblAccountItemTitle.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -32).isActive = true
lblAccountItemTitle.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
Also, I've noticed that the shorter I make my bottom anchor, the less clipped the text is
How can I get rid of the clipping while still maintaining the equal 16 vertical padding?
change the both image and label bottom anchor from equalTo to lessThanOrEqualTo
lblAccountItemTitle.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -16).isActive = true
I was programmatically pinning the label to the cell and not to the Content View
self.topAnchor
should've been
self.contentView.topAnchor

Center Vertically Three UILabels inside UIStackView

I have three UILabels inside an UIStackView. One of them has multiline (last one) and others has only one line. I want them to be centered inside UIStackView so that top and bottom spaces can be dynamic. What am I doing wrong?
headingLabel.translatesAutoresizingMaskIntoConstraints = false
headingLabel.textAlignment = .center
subHeadingLabel.translatesAutoresizingMaskIntoConstraints = false
subHeadingLabel.textAlignment = .center
subHeadingLabel.numberOfLines = 0
bodyLabel.translatesAutoresizingMaskIntoConstraints = false
bodyLabel.textAlignment = .center
bodyLabel.numberOfLines = 0
bodyLabel.text = "My very very long text \n to make it multiline"
textStackView.axis = .vertical
textStackView.distribution = .fillProportionally
textStackView.alignment = .center
textStackView.translatesAutoresizingMaskIntoConstraints = false
textStackView.addArrangedSubview(headingLabel)
textStackView.addArrangedSubview(subHeadingLabel)
textStackView.addArrangedSubview(bodyLabel)
How It is seen now:
How It Should be:
EDIT: I also try to do with fillEqually, but It doesn't change anything. I also set top and bottom anchors of UIStackView. What I want to achieve in here is that for example UIStackView has a height of 100 and all three labels has height of 40. That 60 more space should be equally distributed like 30 - 30 on to and bottom.
Constraints of the UIStackView
myStackView.anchor(contentImageView.bottomAnchor, left: self.view.leftAnchor, bottom: self.view.bottomAnchor, right: self.view.rightAnchor, topConstant: 0, leftConstant: 0, bottomConstant: 0, rightConstant: 0, widthConstant: 0, heightConstant: 0)
One Solution:
I solve it by putting UIStackView inside an UIView and doesn't give height to UIStackView. Just center X and Y anchors. It solve my problem but I'm not sure If It is a proper solution.
Make sure the UIStackView bottom constraint should be greater than equal to zero from UIViewController's view. So that, the UIStackView height will be increased based on the content inside it.
Try this.
let myStackView = UIStackView()
view.addSubview(myStackView)
myStackView.translatesAutoresizingMaskIntoConstraints = false
myStackView.topAnchor.constraint(greaterThanOrEqualTo: view.topAnchor, constant: 0.0).isActive = true
myStackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0.0).isActive = true
myStackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0.0).isActive = true
myStackView.bottomAnchor.constraint(greaterThanOrEqualTo: view.bottomAnchor, constant: 0.0).isActive = true
myStackView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0).isActive = true
myStackView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0.0).isActive = true
What I want to achieve in here is that for example UIStackView has a height of 100 and all three labels has height of 40. That 60 more space should be equally distributed like 30 - 30 on to and bottom.
That is not how a stack view works. If you want three labels with a height of 40 centered then just do that, with no need for a stack view. Or set the stack view height to 40 and center it.

Swift PureLayout: Add x points of padding above and below tallest vertically centered subview

I am using the PureLayout framework and writing my UIView in Swift.
I have a view, which contains four buttons that are not all the same height.
I have added constraints to equally space these buttons, and center them vertically within the view, and added the padding on either side of the buttons on the end. However, I'm not sure how to set the height / top constraints of the view so that it has an 8.0 point gap between it and the top of the tallest button. I can add the constraints to the top and bottom of each button so that they are all >= 8.0 pixels from the edge of the view, but that would mean that the view could stretch vertically while still maintaining those constraints. I won't know which of these buttons is the tallest until runtime.
The only option I can think of is iterating through my buttons before hand and finding the tallest to store in a variable, and using that while making my constraints, but I was wondering if there's a way to do this with PureLayout a bit more simply, without having to run through all of those objects. Is there a method I'm missing that says something like "Make this value as small as possible while satisfying the rest of the constraints" that I could apply to the height of the view, instead?
let topOffset = CGFloat(8.0)
let bottomOffset = CGFloat(8.0)
button1.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button1, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
button2.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button2, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
button3.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button3, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
button4.autoConstrainAttribute(.top, to: .top, of: self, withOffset: topOffset, relation: NSLayoutRelation.self.autoConstrainAttribute(.bottom, to: .bottom, of: button4, withOffset: topOffset, relation: NSLayoutRelation.greaterThanOrEqual)
//MISSING: Somehow make self's height as small as possible
I apologize for the sloppiness, but here is in image of what I'm aiming for sort of. Ignore the horizontal spacing. The rectangle is the view, and the circles are the buttons, with varying sizes. The view needs to have 8.0 points of padding above/below the tallest of these buttons, i.e. the blue one in this case. They are all centered vertically within the view, and I don't know their sizes until I dynamically set the images while instantiating the view.
For what I can understand, this is what you are trying to achieve, right?
To achieve this, you could use an UIStackView with equal spacing as a container, and add all your buttons as children to it. The stack view will automatically assume the height of the tallest button, and that you can space out 8px from the top.
You could use a mixture of a container view and a stackview like this:
override func viewDidLoad() {
super.viewDidLoad()
let container = UIView()
container.translatesAutoresizingMaskIntoConstraints = false
container.backgroundColor = .lightGray
view.addSubview(container)
container.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
container.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .horizontal
stackView.alignment = .center
stackView.distribution = .fillProportionally
stackView.spacing = 8
container.addSubview(stackView)
stackView.topAnchor.constraint(equalTo: container.topAnchor, constant: 8).isActive = true
stackView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8).isActive = true
container.bottomAnchor.constraint(equalTo: stackView.bottomAnchor, constant: 8).isActive = true
container.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: 8).isActive = true
let buttonHeights: [CGFloat] = [30, 50, 40, 60]
for (i, buttonHeight) in buttonHeights.enumerated() {
let button = RoundButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.backgroundColor = .darkGray
button.setTitle("\(i)", for: .normal)
button.setTitleColor(.white, for: .normal)
stackView.addArrangedSubview(button)
button.heightAnchor.constraint(equalToConstant: buttonHeight).isActive = true
button.widthAnchor.constraint(equalTo: button.heightAnchor).isActive = true
}
}
Note: My custom RoundButton class simply sets the corner radius to make it a round button.
Result:

Resources