Center Vertically Three UILabels inside UIStackView - ios

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.

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

Setting a bottom constraint to a UITextView if needed

I'm trying to fix an issue with a UItextview which I placed at the bottom of a viewcontroller programmatically and sometimes it can clip through the bottom of the view if I don't set a constraint like so.
https://i.stack.imgur.com/YfyPi.png
Whenever I try to constraint the textview to the bottom of the safe area, the text needlessly expands too much if there's less text.
https://i.stack.imgur.com/8w1v2.png
Here's the relevant code snippets from the textview and the constraints respectively:
private let summaryTextView: UITextView = {
let summaryTextView = UITextView()
summaryTextView.textColor = .label
summaryTextView.backgroundColor = .customWhite
summaryTextView.textAlignment = .center
summaryTextView.font = UIFont.systemFont(ofSize: 24)
summaryTextView.clipsToBounds = true
summaryTextView.layer.cornerRadius = 20
summaryTextView.layer.masksToBounds = true
summaryTextView.isSelectable = false
summaryTextView.isEditable = false
summaryTextView.isScrollEnabled = false
summaryTextView.translatesAutoresizingMaskIntoConstraints = false
return summaryTextView
}()
private func setupConstraints() {
NSLayoutConstraint.activate([
imageContainerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 120),
imageContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
imageContainerView.heightAnchor.constraint(equalToConstant: 280),
imageContainerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.4),
summaryTextView.topAnchor.constraint(equalTo: imageContainerView.bottomAnchor,constant: 15),
summaryTextView.leadingAnchor.constraint(equalTo: view.leadingAnchor,constant: 10),
summaryTextView.trailingAnchor.constraint(equalTo: view.trailingAnchor,constant: -10),
summaryTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
backgroundImage.fillSuperView(to: view)
bookCover.fillSuperView(to: imageContainerView)
}
Any help would be appreciated!
You need to set the height constraint for UITextView(). Because you are giving fixed top anchor and and bottom anchor so it stretches the textview.

Set constraint on label from below collection view programmatically

Set constraint lbl_Title from bottom to collectionView.
On setting the bottom constraint 60, the label goes below the collection view, after setting it to -60 then it's adjusted to location.
How to set constraints based on collection?
func setCollectionViewConstraints() -> Void {
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: 10).isActive = true
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 0).isActive = true
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 0).isActive = true
collectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 60).isActive = true
}
func setRecentJobLabelConstraints() -> Void {
lbl_Title.translatesAutoresizingMaskIntoConstraints = false
lbl_Title.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -60).isActive = true
lbl_Title.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20).isActive = true
lbl_Title.heightAnchor.constraint(greaterThanOrEqualToConstant: 20).isActive = true
}
Here the issue is fixed if the constraint is set to -60, I think it's the wrong way.
Setting -60 is the right way. The coordinate system for CocoaTouch is a bit strange because it's (0,0) is in the top-left corner of the device, compared to the coordinated in Cocoa which starts from bottom-left. You'll get used to this once you do more auto-layout programmatically.
Note: Also, you need to give negative values when trying to constraint sub-views to super-views from right.
Different Approach: Another approach would be to constraint the super-view to the sub-view this way it's more readable and self-explanatory. Constraint the bottomAnchor of super-view to sub-view's bottomAnchor with a padding of 60 points.
bottomAnchor.constraint(equalTo: lbl_Title.bottomAnchor, constant: 60).isActive = true
It is not a wrong way , calling the constant while using bottom & trailing constraints should be with a minus value , you can use the below extension i created rather than repeating the same autolayout lines over & over
// MARK: - Anchors Method
extension UIView {
func anchors (top:NSLayoutYAxisAnchor? , leading:NSLayoutXAxisAnchor? , bottom : NSLayoutYAxisAnchor? , trailing: NSLayoutXAxisAnchor? , padding : UIEdgeInsets = .zero){
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top , constant: padding.top).isActive = true
}
if let leading = leading {
leadingAnchor.constraint(equalTo: leading , constant: padding.left).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom , constant: -padding.bottom).isActive = true
}
if let trailing = trailing {
trailingAnchor.constraint(equalTo: trailing , constant: -padding.right).isActive = true
}
}
}
and call it like below this :
YourUIView.anchors(top: View.topAnchor , leading: View.leadingAnchor , bottom: View.bottomAnchor , trailing: View.trailingAnchor , padding: .init(top: 10, left: 10, bottom: 10, right: 10))
A quick advice , there is no need to assign a void as a return since the function is not returning something .

layoutMarginsGuide producing unwanted leading and trailing margins

I'm creating constraints programmatically on my views. When I try to use the layoutMarginsGuide anchors, the top and bottom anchors work as expected, but the leading and trailing anchors create margins even if the insets are set at 0. What is creating these unwanted margins and how can I set them correctly?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemRed
view.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)
let childView = UIView(frame: .zero)
childView.backgroundColor = .systemIndigo
childView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(childView)
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
childView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
childView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
childView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
])
}
So that the side contraints do not contain spaces and are completely glued to the sides, you have to remove the layoutMarginsGuide, it would be like:
NSLayoutConstraint.activate([
childView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
childView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
childView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
childView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
])
Unlike other views, the system manages the margins of a view controller's root view. By default, it enforces minimum left and right margins of either 16 or 20 points depending on the view width. The top and bottom margins are by default zero.
So if you want less margin of root view then system minimum. you have to make false to viewRespectsSystemMinimumLayoutMargins
viewRespectsSystemMinimumLayoutMargins = false
view.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)

Why constraints are updated on scrolling of UITableView?

I have 2 simple UILabel that needs to be placed horizontally. One of them on right side of view, while second one is on the left side and gets all possible space. Here is my constraints:
amountOfQuestions.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true
amountOfQuestions.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
topicName.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
topicName.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true
topicName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8).isActive = true
topicName.rightAnchor.constraint(equalTo: amountOfQuestions.leftAnchor, constant: -8).isActive = true
Seems like everything is ok, but when table view is shown, it looks like this:
But after I scroll couple of times my table up and down, it became normal:
Why my table is not shown immediately like in second picture?
Solution:
amountOfQuestions.rightAnchor.constraint(equalTo: rightAnchor, constant: -8).isActive = true
amountOfQuestions.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
amountOfQuestions.setContentHuggingPriority(.required, for: .horizontal)
amountOfQuestions.setContentCompressionResistancePriority(.required, for: .horizontal)
topicName.leftAnchor.constraint(equalTo: leftAnchor, constant: 8).isActive = true
topicName.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true
topicName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8).isActive = true
topicName.rightAnchor.constraint(equalTo: amountOfQuestions.leftAnchor, constant: -8).isActive = true
topicName.setContentHuggingPriority(.required, for: .horizontal)
topicName.setContentCompressionResistancePriority(.required, for: .vertical)
let myBottom = topicName.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8)
myBottom.priority = UILayoutPriority(rawValue: UILayoutPriority.required.rawValue - 1)
myBottom.isActive = true
There are multiple problems that can affect the layout and make it incorrectly defined:
Never constrain content to the cell itself. Always constrain to the contentView of the cell, e.g.:
amountOfQuestions.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -8).isActive = true
(make sure you are adding the labels as subviews of the contentView, not of the cell itself).
If you want the size of the labels to have any effect, you need to set hugging and compression resistance priorities correctly. Otherwise the layout does not have to respect the content:
// you want topic name to resize vertically to respect the content
topicName.setHuggingPriority(.required, for: .vertical)
topicName.setContentCompressionResistancePriority(.required, for: .vertical)
// you want amount of question to resize horizontally to respect the content
amountOfQuestions.setHuggingPriority(.required, for: .horizontal)
amountOfQuestions.setContentCompressionResistancePriority(.required, for: .horizontal)
To avoid layout conflicts during the first layout, it's a good idea to give one the of the vertical constraints a slightly lower layout priority:
let bottomAnchor = topicName.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8)
bottomAnchor.priority = UILayoutPriority(rawValue: UILayoutPriority.required.rawValue - 1)
bottomAnchor.isActive = true
1- UILabels have an intrinsic content value , that means you don't have to specify a width / height for them , so when you place them horizontally like this
| - lbl1 - lbl2 - |
This causes a constraint ambiguity because autolayout need to know which label to trim when a one content is too bigger that's why you need to set horizontal compression resistance && setHuggingPriority to lbl2 = 1000
2- You need to lower priority of bottom constraint as the cell assumes a fixed height in the begining of layout and that height may conflict with current total height of the vertical content , so you lower it to avoid this conflict

Resources