Can UIStackView be constrained? - ios

I have a UIScrollView that fills all the window and that contains a vertical UIStackView where I want to add UILabels that fill all available width.
The UIStackView is pinned to all the edges of UIScrollView, and I expect it to fill all the UIScrollView. But it seems that it completely ignores all the constriants and it only sizes to fit it's childs. Why that behaviour?
To illustrate that if I add labels that way:
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
scrollView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[stackView]|", options: NSLayoutFormatOptions.alignAllCenterX, metrics: nil, views: ["stackView": stackView]))
for _ in 1 ... 20 {
let textLabel = UILabel()
textLabel.backgroundColor = UIColor.yellow
textLabel.text = "Hi World xxxx"
stackView.addArrangedSubview(textLabel)
textLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = true //Doesn't work as expected
}
what I get is this: (the cyan background is the UIScrollView and you can see that labels doesn't take all width due to that UIStackView contraints doesn't apply)
And only if I change the label contraint to use scrollView it works as desired:
textLabel.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
Then the result is as expected:
Why that behaviour? Someone can explain why the constraints on UIStackView doesn't work as I expect?
Complete source code avaiable at that gist

Everything is correct. Its just that you have not specified the width of the content view(the stack view).
Add this line and thing will work perfectly.
scrollView.widthAnchor.constraint(equalTo: stackView.widthAnchor, multiplier: 1).isActive = true;
Note: Pinning scroll view's leading trailing with content view does, not provide the width to the content view. A separate width constraint is required, other wise the content view will have its width equal to its intrinsic width.
Links: https://stackoverflow.com/a/18341997/1790683

Related

How to vertically align two UILabels within one line?

I am trying to put two UILabels within one line: one UILable on left side, with text left-aligned, one UILabel on right side, with text right-aligned. Please see above image. They have different font sizes.
I used below auto layout constraints:
let labelHorizontalConstrains = NSLayoutConstraint.constraints(
withVisualFormat: "H:|-20-[nameLabel]-15-[birthDeathDateLabel]-20-|",
metrics: nil,
views: views)
allConstraints += labelHorizontalConstrains
But the result shows that they are not vertically aligned: The left UILabel is lower than the right UILabel. How to fix this?
You need to add a centerYAnchor constraint.
birthDeathDateLabel.centerYAnchor.constraint(equalTo: nameLabel.centerYAnchor).isActive = true
However, if you want it aligned with the base of the text, you should use a UIStackView.
let stackView = UIStackView(arrangedSubviews: [nameLabel, birthDeathDateLabel])
stackView.alignment = .firstBaseline
stackView.axis = .horizontal
// Add other stackview properties and constraints
you need to add a leading constraint and a height constraint to the right label. add a trailing and height constraint to the left label. then specify an equal widths constraint. also on the right label add the distance from the left label with a trailing constraint and for the left label add a leading constraint how far you want it from the right label.. much easier on storyboard. you could also use fit equally.. I can't remember the actual code, but it might have refreshed your memory. if I was too add that sort of constraint I would do it on a skeleton storyboard.

Add subviews programmatically to ScrollView using Visual Format Language

scroll view - layout programmatically - swift 3
Dear friends: I hope someone could revise this project and help me before my brain been burned. Thanks in advance.
The task: Horizontal Scroll - Layout an array of 4 images, square of 240 x 240 and 20 of spacing. The constraints for the scroll view set directly in the storyboard, but the images subviews had been added programmatically using Visual Format Language. Content size for scroll suppose done by this constraints.
What I have done: Set the array of images, create de ImageView programmatically and add the array using a for in loop. Create the constraints using the visual format. A way to do this can be found in this article: http://www.apeth.com/iOSBook/ch20.html.
Here the link to the project in GitHub
https://github.com/ricardovaldes/soloScrollEjercicio
Constraints for the ScrollView added directly in the storyboard.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myScroll: UIScrollView!
var carsArray = [UIImage]()
var constraints = [NSLayoutConstraint]()
override func viewDidLoad() {
super.viewDidLoad()
carsArray = [#imageLiteral(resourceName: "fasto1"), #imageLiteral(resourceName: "fasto2"), #imageLiteral(resourceName: "fasto3"), #imageLiteral(resourceName: "fasto4")]
var const = [NSLayoutConstraint]()
var views: [String: UIView]
var previous: UIImageView? = nil
for index in 0..<carsArray.count{
let newImageView = UIImageView()
newImageView.image = carsArray[index]
newImageView.translatesAutoresizingMaskIntoConstraints = false
myScroll.addSubview(newImageView)
self.myScroll.setNeedsLayout()
views = ["newImageView": newImageView, "myScroll": myScroll]
if previous == nil{
const.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:|[newImageView(240)]", metrics: nil, views: views))
}
else{
const.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[previous]-20-[newImageView(240)]", metrics: nil, views: ["newImageView": newImageView, "previous": previous!]))
}
previous = newImageView
const.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "H:[previous]|", metrics: nil, views: ["previous": newImageView]))
const.append(contentsOf: NSLayoutConstraint.constraints(withVisualFormat: "V:|[newImageView(240)]|", metrics: nil, views: views))
}
NSLayoutConstraint.activate(const)
}
}
Even though I have tried a lot of combinations I have the same error:
2018-04-29 21:24:34.347466-0500 soloScrollEjercicio[12002:1665919] [LayoutConstraints] Unable to simultaneously satisfy constraints.
each loop round, you add a right side pin constraint to scroll view for every new
added imageView, but there is 20 points between each other.
|-previous-20-newOne-| ** second round loop **
-20-newOne'-| ** third round loop **
this breaks imageView width(240) constraint
one way deal with it:
only add right side pin constraint to the last imageView.
scroll view constraint in your main storyboard also has a break.
+-------------------+
| | |
|-scroll view (240)-|
the bottom one with vertical spacing to super view should not be there. it would
break the scroll view height(240), so delete it will be fine.
maybe you should try:
set it's constraint priority to 999, or some other value not equal to
1000
uncheck installed box
delete it
and now, your scroll view should be OK.
p.s. I found your reference book is based on iOS 6? in the year 2018, starts from iOS 10 or iOS 11 may be a better choice.
happy hacking :)

How to make an evenly spaced row of images with AutoLayout? [duplicate]

This question already has answers here:
Evenly space multiple views within a container view
(29 answers)
Closed 6 years ago.
I've just started working on this card game in Swift, and I was trying to figure out how to lay out a row of 6 cards horizontally near the top of the screen.
I've tried putting 6 imageViews in a stack, but my manual constraints ended up causing the last image to stretch to the edge:
Could someone show me how to set up a row of imageViews so that each of them has a fixed width and they're all centered? I'm kinda new to AutoLayout, so screenshots would be helpful.
I'd recommend using UIStackView. Have a look at the following Ray Wenderlich tutorial:
https://www.raywenderlich.com/114552/uistackview-tutorial-introducing-stack-views
However, before moving on to more complex views such as the aforementioned stack view; you should learn to use auto layout to avoid making any silly mistakes.
Here is another great tutorial from the same site:
https://www.raywenderlich.com/115440/auto-layout-tutorial-in-ios-9-part-1-getting-started-2
EDIT:
Improved answer:
UIStackView allows you to arrange elements with ease, in a row or in a column. This saves you a lot of time and makes your storyboard look a little bit cleaner as less constraints are needed.
The description of UIStackView on developer.apple.com:
The UIStackView class provides a streamlined interface for laying out a collection of views in either a column or a row. Stack views let you leverage the power of Auto Layout, creating user interfaces that can dynamically adapt to the device’s orientation, screen size, and any changes in the available space. The stack view manages the layout of all the views in its arrangedSubviews property. These views are arranged along the stack view’s axis, based on their order in the arrangedSubviews array. The exact layout varies depending on the stack view’s axis, distribution, alignment, spacing, and other properties.
UIStackViews functionality doesn't stop at the simplified view alignement. Indeed, you can also alter the properties that define the stack view.
The axis property determines the stack’s orientation, either
vertically or horizontally.
The distribution property determines the layout of the arranged views
along the stack’s axis.
The alignment property determines the layout of the arranged views
perpendicular to the stack’s axis.
The spacing property determines the minimum spacing between arranged
views.
The baselineRelativeArrangement property determines whether the
vertical spacing between views is measured from the baselines.
The layoutMarginsRelativeArrangement property determines whether the
stack view lays out its arranged views relative to its layout margins.
Despite the advantages mentioned above, UIStackView has limits.
The UIStackView is a nonrendering subclass of UIView; that is, it does
not provide any user interface of its own. Instead, it just manages
the position and size of its arranged views. As a result, some
properties (like backgroundColor) have no effect on the stack view.
Similarly, you cannot override layerClass, drawRect:, or
drawLayer:inContext:.
Note that UIStackView can't scroll. If you ever need it to scroll, embed a stack view within a UIScrollView.
Hope this helps!
I recommend pure coding, you learn more.
If you specify that all your cards are equal width and height, it will ensure the last card doesn't get stretched.
This is how I often build my UI:
import UIKit
class ViewController: UIViewController {
var container:UIView = UIView();
var card1:UIView! = nil;
var card2:UIView! = nil;
var card3:UIView! = nil;
var card4:UIView! = nil;
var card5:UIView! = nil;
var card6:UIView! = nil;
override func viewDidLoad() {
super.viewDidLoad()
self.initViews();
self.initConstraints();
}
func cardView() -> UIView
{
let card = UIView();
card.backgroundColor = UIColor.orangeColor();
return card;
}
func initViews()
{
self.card1 = self.cardView();
self.card2 = self.cardView();
self.card3 = self.cardView();
self.card4 = self.cardView();
self.card5 = self.cardView();
self.card6 = self.cardView();
self.container.addSubview(self.card1);
self.container.addSubview(self.card2);
self.container.addSubview(self.card3);
self.container.addSubview(self.card4);
self.container.addSubview(self.card5);
self.container.addSubview(self.card6);
self.view.addSubview(self.container);
}
func initConstraints()
{
self.container.translatesAutoresizingMaskIntoConstraints = false;
self.card1.translatesAutoresizingMaskIntoConstraints = false;
self.card2.translatesAutoresizingMaskIntoConstraints = false;
self.card3.translatesAutoresizingMaskIntoConstraints = false;
self.card4.translatesAutoresizingMaskIntoConstraints = false;
self.card5.translatesAutoresizingMaskIntoConstraints = false;
self.card6.translatesAutoresizingMaskIntoConstraints = false;
var views = [String: AnyObject]();
views["container"] = self.container;
views["card1"] = self.card1;
views["card2"] = self.card2;
views["card3"] = self.card3;
views["card4"] = self.card4;
views["card5"] = self.card5;
views["card6"] = self.card6;
self.view.addConstraint(NSLayoutConstraint(item: self.container, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: self.view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0.0));
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-50-[container]", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[card1(60)]-10-[card2(==card1)]-10-[card3(==card1)]-10-[card4(==card1)]-10-[card5(==card1)]-10-[card6(==card1)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[card1(100)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[card2(==card1)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[card3(==card1)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[card4(==card1)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[card5(==card1)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
self.container.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[card6(==card1)]|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: views));
}
}
You end up with something like this:

Auto Layout: empty UITextField is only wide enough for 1 character

I'm trying to set up a simple UITableViewCell in which there is a prompt on the left and then a text field right next to it that takes up the rest of the width of the cell; however, with the following constraints, no matter what I set the width of the UITextField to, the width of the field is only wide enough for 1 character. Note: the UITextField is appearing in the correct position (it's directly to the right of the prompt), it's just really small.
cell.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[prompt]-[uitextfield]", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: views))
cell.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[promt]-|", options: nil, metrics: nil, views: views))
cell.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-[uitextfield]-|", options: nil, metrics: nil, views: views))
One solution that has worked is to set the width of the uitextfield to a ridiculously high number:
cell.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[prompt]-[uitextfield(1000)]", options: NSLayoutFormatOptions.AlignAllCenterY, metrics: nil, views: views))
I can't use this solution however because sometimes the UITextField is a UISegmentedControl (or other control), and I don't want to stretch those controls out.
Why is the UITextField forcing its width to be really small even if I explicitly set frame.size.width to something large?
The UITextField's content hugging priority along the horizontal axis should be lower than your prompt element's content hugging priority.
You can check out the content hugging priority by doing yourTextField.contentHuggingPriorityForAxis(.Horizontal). And set it using yourTextField.setContentHuggingPriority(250, forAxis: .Horizontal). You may also do this in the interface builder (which I prefer).
Try setting the text field's content hugging priority to 250, and set prompt's to 251.
Solution:
1) Pin the UITextField to the right edge of its superview
2) Change the hugging priority of the UITextField to less than 250
Can someone explain why this works? Why doesn't it work to just do step #1? Or, if you only do step #1, why don't prompt & uitextfield expand equally to fill up the extra width in the superview?

Auto layout - Collapsable views (vertically)

I'd like to create an "adding a new credit card viewController".
We don't want to aggravate users with all of the required fields presented at once.
This action contains several steps.
On each step the view-controller reveals a new subview (which contains one or more textfields) and collapses an old one (the current text field after it's text is validated).
I've created the ViewController on the storyboard. and placed all of its subviews one above the other.
I've created all of the constraints on the storyBoard, each subviews' clips to the above subview etc'.
i.e:
NSMutableArray *constraints = [[NSLayoutConstraint
constraintsWithVisualFormat:
#"V:|[titleView]-[subtitleView]-[amountView]-[cardNumView]-[cardsImagesView]-[mmYYCvvView]-[billingInfoView]-[buttomView]|"
options:NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
metrics:nil
views:variableBindings] mutableCopy];
Each of these subviews contain a height constraint.
In each step one of the height constraints are set to zero and another one is changed from zero to the required height.
i.e:
self.hgtCrtMMYYCvv.constant = showFields? 50 : 0;
self.hgtCrtBillingInfo.constant = showFields? 140 : 0;
self.mmYYCvvView.hidden = !showFields;
self.billingInfoView.hidden = !showFields;
I got two issues:
Without calling layoutIfNeeded the initial layout was valid but did not change after changing the height constraints.
Calling layoutIfNeeded did not clip the bottom view to the last visible one - placed it at the bottom of the view as if all the subviews appear at once, but since some are hidden a gap was created.
changing the height constraint of the subviews was applied on the screen but still the gap stayed.
Please advise.
Calling "layoutIfNeeded" did not clip the bottom view to the last visible one - placed it at the bottom of the view as if all the subviews appear at once
Look at your constraints. You have pinned the bottom of the bottom view to the bottom of its superview! So its bottom must appear at the bottom of the superview, since that is what you instructed it to do.
Indeed, I am surprised that your constraints work at all. You have basically overdetermined them. If you give every field a height and pin its top and bottom, for every field, then it will be impossible to satisfy your constraints unless you are very lucky. The height of the superview is fixed, so your constraints would have to add up perfectly to that height.
I'm going to suggest a complete alternative approach, which I think you will find easier. Instead of messing with individual constants, plan what the correct (not overdetermined) constraints would be for each possible situation, and store those constraints in properties. Now when you want to hide/reveal a field, you just remove all the constraints and swap in another set.
This will also solve the layoutIfNeeded problem.
It happens that I have an actual example showing how to do this. (It is written in Swift, but I'm sure you can compensate mentally.) In my example code, we have three rectangles; I then remove one rectangle and close the gap between the remaining two. The preparation of two sets of constraints is tedious but elementary:
let c1 = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(20)-[v(100)]", options: nil, metrics: nil, views: ["v":v1]) as [NSLayoutConstraint]
let c2 = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(20)-[v(100)]", options: nil, metrics: nil, views: ["v":v2]) as [NSLayoutConstraint]
let c3 = NSLayoutConstraint.constraintsWithVisualFormat("H:|-(20)-[v(100)]", options: nil, metrics: nil, views: ["v":v3]) as [NSLayoutConstraint]
let c4 = NSLayoutConstraint.constraintsWithVisualFormat("V:|-(100)-[v(20)]", options: nil, metrics: nil, views: ["v":v1]) as [NSLayoutConstraint]
let c5with = NSLayoutConstraint.constraintsWithVisualFormat("V:[v1]-(20)-[v2(20)]-(20)-[v3(20)]", options: nil, metrics: nil, views: ["v1":v1, "v2":v2, "v3":v3]) as [NSLayoutConstraint]
let c5without = NSLayoutConstraint.constraintsWithVisualFormat("V:[v1]-(20)-[v3(20)]", options: nil, metrics: nil, views: ["v1":v1, "v3":v3]) as [NSLayoutConstraint]
self.constraintsWith.extend(c1)
self.constraintsWith.extend(c2)
self.constraintsWith.extend(c3)
self.constraintsWith.extend(c4)
self.constraintsWith.extend(c5with)
self.constraintsWithout.extend(c1)
self.constraintsWithout.extend(c3)
self.constraintsWithout.extend(c4)
self.constraintsWithout.extend(c5without)
NSLayoutConstraint.activateConstraints(self.constraintsWith)
But the payoff comes when it is time to swap the middle view in or out of the interface: it's trivial. Just remove or insert it, and then remove all constraints and now insert the complete new set of constraints appropriate to the situation, which we have already prepared:
#IBAction func doSwap(sender: AnyObject) {
if self.v2.superview != nil {
self.v2.removeFromSuperview()
NSLayoutConstraint.deactivateConstraints(self.constraintsWith)
NSLayoutConstraint.activateConstraints(self.constraintsWithout)
} else {
self.view.addSubview(v2)
NSLayoutConstraint.deactivateConstraints(self.constraintsWithout)
NSLayoutConstraint.activateConstraints(self.constraintsWith)
}
}
The preparation of the multiple sets of constraints is tedious but can be done by rule, i.e. the constraints can be "machine-generated" in a loop (writing this is left as an exercise for you). Swapping constraints in and out is again according to a simple rule, since only one set will be right for the particular set of fields you wish to show/hide. So once this is set up it will be much simpler and more maintainable than what you are doing now.

Resources