How can I avoid specifying potentially conflicting constraints - swift

I am trying to create all of my views in my UICollectionViewCell programatically. The views currently lay out correctly.
I have a vertical stackView that contains two UILabels and a UIImageView.
The views do not have a fixed content size and are all variable.
If you see in my code, I am setting the leading and trailing constraints of my UIStackView with a constant of 8.0 from the safeAreaLayoutGuide. Because of this, I have to set the widthAnchor to be -16.0 (otherwise the cell does not lay out correctly). Is there a way I can avoid specifying this in two places? For example, if I set the leading to be 14.0 points from the leading, then I will have to make sure I remember to change my width to be -20.0, otherwise the contents will spill over.
override func updateConstraints() {
/// Check if views already have constraints
if !viewsHaveConstraints {
viewsHaveConstraints = true
/*
Here I set the hugging and compression priority for the 3 views in the UIStackView
*/
let margins = safeAreaLayoutGuide
/* Set vstackView constraints */
/// vStackView width anchor equal to width of margin less constant
vStackView.widthAnchor.constraint(equalTo: margins.widthAnchor, constant: -16.0).isActive = true
/// vStackView leading anchor constraint
vStackView.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 8.0).isActive = true
/// vStackView trailing anchor constraint
vStackView.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 8.0).isActive = true
vStackView.heightAnchor.constraint(equalTo: margins.heightAnchor).isActive = true
}
super.updateConstraints()
}

Related

How do I make a Swift UIStackView size dynamically based on content?

I have added the following UIStackView to my ViewController. As the views change the values of textOneLabel and textTwoLabel change.
With the following code the initial StackView is centered and the portions filled proportionally. However with subsequent text combinations the bounds of the StackView don't change, leaving the content off center. How can I change the StackView properties so it will adapt to the content and always stay centered?
headerStackView.axis = .horizontal
headerStackView.distribution = .fillProportionally
headerStackView.spacing = 8
headerStackView.layer.borderWidth = 1
headerStackView.layer.borderColor = UIColor.red.cgColor
headerStackView.translatesAutoresizingMaskIntoConstraints = false
headerStackView.topAnchor.constraint(equalTo: timerHeader.topAnchor, constant: 4).isActive = true
headerStackView.centerXAnchor.constraint(equalTo: timerHeader.centerXAnchor).isActive = true
headerStackView.addArrangedSubview(textOneLabel)
headerStackView.addArrangedSubview(textTwoLabel)
First, forget you ever heard of .fillProportionally...
it doesn't do what you think it does
you'll encounter very unexpected layout issues if your stack view has spacing greater than Zero
if your stack view has no width (neither width anchor nor leading/trailing anchors), .fillProportionally doesn't do anything
So, change your .distribution to .fill.
Add these lines to control what auto-layout does with your labels:
textOneLabel.setContentHuggingPriority(.required, for: .horizontal)
textOneLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
textTwoLabel.setContentHuggingPriority(.required, for: .horizontal)
textTwoLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
Now, your stackView FRAME will remain centered.

UIStackView and a placeholder view in another UIStackView problem

There is a problem if you have a UIStackView(testStack) and a placeholder UIView(testView) inside another UIStackView(mainStack). It is meant that if there is no content in the testStack it will collapse, and the testView will take all the space. There is even a content hugging priority set to maximum for the testStack so it should collapse its height to 0 when there are no subviews. But it does not. How to make it collapse when there is no content?
PS If there are items in the testStack, everything works as expected: testView takes all available space, testStack takes only the space to fit its subviews.
class AView: UIView {
lazy var mainStack: UIStackView = {
let stack = UIStackView()
stack.axis = .vertical
stack.backgroundColor = .gray
stack.addArrangedSubview(self.testStack)
stack.addArrangedSubview(self.testView)
return stack
}()
let testStack: UIStackView = {
let stack = UIStackView()
stack.backgroundColor = .blue
stack.setContentHuggingPriority(.init(1000), for: .vertical)
return stack
}()
let testView: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
init() {
super.init(frame: .zero)
backgroundColor = .yellow
addSubview(mainStack)
mainStack.translatesAutoresizingMaskIntoConstraints = false
mainStack.topAnchor.constraint(equalTo: topAnchor).isActive = true
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
mainStack.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
mainStack.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
When auto-layout arranges subviews in a UIStackView, it looks at:
the stack view's .distribution property
the subviews' height constraints (if given)
the subviews' Intrinsic Content Size
Since you have not specified a .distribution property, mainStack is using the default of .fill.
A UIStackView has NO Intrinsic Content Size, so auto-layout says "testStack has a height of Zero"
A UIView has NO Intrinsic Content Size, so auto-layout says "testView has a height of Zero"
Since the distribution is fill, auto-layout effectively says: "the heights of the arranged subviews are ambiguous, so let's give the last subview a height of Zero, and fill mainStack with the first subview.
Setting .setContentHuggingPriority will have no effect, because there is no intrinsic height to "hug."
If you set mainStack's .distribution = .fillEqually, you will get (as expected) testStack filling the top half, and testView filling the bottom half.
If you set mainStack's .distribution = .fillProportionally, you will get the same result... testStack filling the top half, and testView filling the bottom half, because .fillProportionally uses the arranged subviews' Intrinsic Content Sizes... in this case, they are both Zero, so "proportional" will be equal.
If you set mainStack's .distribution = .equalSpacing or .distribution = .equalCentering, you won't see either testStack or testView ... auto-layout will give each of them a height of Zero, and fill the rest of mainStack with (empty) "spacing."
If your goal is to have testStack "disappear" if it is empty, you can either:
set it hidden, or
subclass it and give it an intrinsic height

Swift - How to set spacing between variable number of buttons in Horizontal StackView?

I'm using a horizontal UIStackView that can contain up to 3 UIButtons. (could be just 1, could be 2, or 3)
I want to have them centered, with the same space between each of them, like so:
I don't seem to be able to pull this off... I've tried every combination of Distribution and Alignment, but I can’t get it right.
Here's the stackView's constraints:
fileprivate func setupStackViewConstraints() {
stackView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalToConstant: 60),
stackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
And here's the buttons' constraints:
fileprivate func setupFacebookButton() {
let facebookButton = UIButton()
facebookButton.setImage(UIImage(named: "facebookSocialIcon"), for: .normal)
facebookButton.imageView?.contentMode = .scaleAspectFit
facebookButton.addTarget(self, action: #selector(facebookButtonWasTapped), for: .touchUpInside)
// -----------------------------------
facebookButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
facebookButton.widthAnchor.constraint(equalToConstant: 40),
facebookButton.heightAnchor.constraint(equalToConstant: 40)
])
// -----------------------------------
stackView.addArrangedSubview(facebookButton)
}
Any ideas?
The distribution and alignment are obvious here. You want "Equal Spacing" and "Center". You should also set a suitable spacing.
The "hard bit" is how to get the arranged views to horizontally align in the middle, since the alignment property is only about alignment perpendicular to axis.
The key is to let the stack view have a flexible width, and align it horizontally in the center. This means removing the leading and trailing constraints, and adding a center X constraint.
Therefore, these are the constraints on your stack view:
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalToConstant: 60),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
Note that the stack view will extend its width to fit all the arranged subviews in it. This means that if you have too many arranged subviews, some of them might go offscreen. If you want them to "wrap" to the next line, consider using a UICollectionView instead :)
Try constraining your StackView to center x and y only (not constraining it's leading and trailing anchors):
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
And then you can leave the distribution value to it's default and setting the spacing value to what you want:
stackView.spacing = // 16px
This way the StackView's width will be equal to the content width.

Add layoutMargins to one element in a UIStackView

I would like to create a vertical stackview with 3 elements in it.
I want a bit more space only between the 2nd and the last element. So I thought about adding to the last element :
mylastelement.layoutMargins = UIEdgeInsets(top:30, left:0,bottom:0, right:0)
But the layoutmargins are not applied in my stackview. Is there any easy way to achieve that (Id like to avoid to modify the last element inner height).
EDIT : I just tried to increase 2nd element height (+50) within its frame by doing :
my2ndElementLabel.sizeToFit()
my2ndElementLabel.frame = CGRect(x:my2ndElementLabel.frame.origin.x,y:lmy2ndElementLabel.frame.origin.y,
width:my2ndElementLabel.frame.width, height:my2ndElementLabel.frame.height + 50)
but it has no effect.
EDIT2 : I tried to add a random view to my UIStackView, but the the view is just ignored ! May have missed something in understanding how UIKit work ?... :
let v = UIView(frame:CGRect(x:0,y:0,width:100,height:400))
v.backgroundColor = .red
myStackView.addArrangedSubview(v)
//...
Here is an extension I made that helps to achieve fast such margins :
extension UIStackView {
func addArrangedSubview(_ v:UIView, withMargin m:UIEdgeInsets )
{
let containerForMargin = UIView()
containerForMargin.addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: containerForMargin.topAnchor, constant:m.top ),
v.bottomAnchor.constraint(equalTo: containerForMargin.bottomAnchor, constant: m.bottom ),
v.leftAnchor.constraint(equalTo: containerForMargin.leftAnchor, constant: m.left),
v.rightAnchor.constraint(equalTo: containerForMargin.rightAnchor, constant: m.right)
])
addArrangedSubview(containerForMargin)
}
}
What you could do is set a custom spacing between the second and third element.
myStackView.setCustomSpacing(30.0, after: my2ndElementLabel)
In the same general vein, you can constrain the top (or bottom) anchor of your view relative to the corresponding edge of any view in which it's embedded. What's ugly being somewhat a matter of taste, I find autolayout constraints easy to use and easy to reason about.
A simple example from Mac OS rather than iOS:
let button = ControlFactory.labeledButton("Filter")
addSubview(button)
button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20).isActive = true
button.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
This particular code lives in the view initializer, and positions a button in the middle of a view, 20 points up from the bottom.
I found myself : It looks like UIStackView doesn't work at all with old sizing system (with .frame). It seems you have to constraint height and width, and StackView will constraint left/top/right/bottom position for you when you add the arrangedSubview.
My second view was a label : I wanted a margin of 40, under the text. So i first computed the label height into its .frame property, and constraint the height at frame.height + 40(= my margin)
labelDesc.sizeToFit()
labelDesc.heightAnchor.constraint(equalToConstant:40).isActive = true
I find my own solution utterly ugly though. I'm sure UIKit provide a better way to achieve such a simple goal, without having to make these kind of DIY solutions. So please if you're used to work with UIKit, tell me if there is any better solution.
Consider adding a "margin" by inserting a correctly-sized UIView within the Stack View as needed.
If you need a 40px margin between 2 specific elements... add a UIView with a height constraint of 40px. Assign a clearColor background to make it invisible.
You can add IBOutlets to this view and hide it as you would any other item in the Stack View.

Updating constraints on orientation change

Below is the view in portrait mode (Image 1) and in landscape I wanted to show as (Image 2). I am facing issue to show in it properly in landscape.
Image 1:
I have setup constraints in storyboard.
greenView: top: 0, leading: 0, trailing: 0, width: equal to superview.width, height: equal to superview.height/2
Image 2:
I tried modifying constraints but when I turn device to landscape, greenView becomes 1/4 of the screen. below is the code.
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape {
greenView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.50).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 0).isActive = true
} else {
}
}
Instead of headache of creating constraints for this problem insert both views in a UIStackView (vertical) , and inside viewWillTransition change axis to horizontal if the orientation is isLandscape
plus adding these constraints
greenView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.50).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 0).isActive = true
will make a conflict as the old ones are not removed
//
func shared () {
if UIDevice.current.orientation == .portrait {
self.stView.axis = .vertical
}
else {
self.stView.axis = .horizontal
}
}
Call the above method in viewDidLoad & viewWillTransition
isActive flag is highly misunderstood option. This flag does not change constraint's state, it completely adds or removes a constraint.
greenView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.50).isActive = true
greenView.heightAnchor.constraint(equalToConstant: 0).isActive = true
The above code will add multiple constraint on your view. Every time you rotate your device a new width, height constraint gets added to your view which will result in your view having multiple height and width constraints. To add/remove same constraint, store its reference then use isActive on it.
I'm not sure why you are setting height constraint to 0?
Now coming to what you want to do. I can think of 2 approaches
1st Approach
Add two more constraints in addition to existing constraints in your storyboard but keep their priority low(<1000):
1. greenView.bottom = safeArea.bottom
2. greenView.width = superView.width/2
Make IBOutlet of greenView.height = superview.height/2 and greenView.trailing = superView.trailing. The outlets should be of those constraints which have high priority. Make sure your Outlets are not weak otherwise their outlet will become nil when you set isActive false. Now all you have to do is set this when device changes to landscape mode:
highPriorityGreenViewConstraint.isActive = false
highPriorityHeightConstraint.isActive = false
2nd Approach
Use size classes to set your constraint. All size classes are mentioned here.
Example - Install greenView.bottom = safeArea.bottom,greenView.width = superView.width/2 constraints for compact width compact height size class only. You will have to put more constraints in this approach as landscape size class is different even among iPhone models.