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

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:

Related

Can UIStackView be constrained?

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

Adding Subview creates layout issue

I'm programmatically adding a view inside another view like so:
func addViewControllerToSpecificView( view: UIView, controller: UIViewController) {
controller.willMoveToParentViewController(self)
view.addSubview(controller.view)
self.addChildViewController(controller)
controller.didMoveToParentViewController(self)
}
The issue is that the parent view ends up being wider than it should be (the width of the screen).
When I don't load the subview using the above method, the positioning is perfect (no extra padding). I have no idea why it's adding an extra ~30px
add following line in your code....
controller.view.frame = self.view.layer.bounds;
this will fix the problem.
I dont know that are you adding Auto layout constraint for this view.
Please check that this layout constraints are added in your code, if not then add below constraint after you add childViewController.
let metrices = ["width" : view.bounds.size.width, "height" : view.bounds.size.height]
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[childView(width)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrices, views: ["childView" : controller.view]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[childView(height)]", options: NSLayoutFormatOptions(rawValue: 0), metrics: metrices, views: ["childView" : controller.view]))

Manage UITextField's dynamic position using auto layout

My client’s requirement is as follows:
I am going to create registration form with around 15 textFields(Pre defined on server). Information of fields in registration fields are coming from server. There are chances that some fields are not required in some cases. So, I will get fields which are required for that particular case.
E.g. Suppose No. of predefined fields are 15 on server. i.e. FirstName, LastName, Phone no., Date of Birth, Picture, Email,Address,zipcode etc.
But there are chances that in some cases email id field is not required. so I will not get that field from server. So, application side I have to hide email textfield from predefined list of fields.
Also there are chances that multiple(any field) fields are not required. So I have to hide them too. I am trying to explain situation by image representation as follows.
Case 1:
Case 2:
In this case, what I am doing is(Application side) I create textFields for all predefined fields. then I am hiding textfield which is not required as per server response. i.e. email textfield or any other textField .
Now my question is, If any field is not required then the textfields should be repositioned and I am using auto layout in my application. I am hiding email textfield then How to set positions of next textfield like contact NO., Date of Birth etc.?
Yes, this is perfectly possible with Auto Layout.
The most important thing here is:
You do not need to reposition it manually, this job will done by Auto layout.
You must do the following things:
Use UIScrollView and add all UITextField in that. So that content size and changing position will be easily managed. Here content size of scrollView is also managed by Auto layout, So don't do it manually.
Don't assign frame manually for UITextField. e.g. By using CGRect when creating textField.
Use VFL (Visual Formatting Language). This is a powerful auto layout language which can gives you power to dynamic changes on runtime using code.
Add constraints for all UITextFields and initially set hidden property to No/False. From your server response and query make them visible and update auto layout constraints by simply calling below methods.
// Change hidden property based on your response
// ........
// ........
// Below methods will update auto layout
[textField layoutIfNeeded];
[self.view layoutIfNeeded];
You can put these method in animation block for ample UI experience.
Note: Remember VFL is not an easy part you must have to take care when you are using it.
Adding sample code which can helps you (But it is in Swift convert it in Objective C), Also there may be a variation in parameters and values of VFL because this code was fit as per my requirement. Have a look and change it as per your need.
Create UIScrollView using VFL:
// Create scrollview to display content
self.scrollView = UIScrollView()
self.scrollView.delegate = self
self.scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
self.view.addSubview(self.scrollView)
// Visual formatting constraints of scrollview horizontal-vertical alignment with respect to view
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["scrollView" : scrollView]))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["scrollView" : scrollView]))
Add UITextField in UIScrollView:
func createRegistrationControls() {
var previousTextField : UITextField! = nil
// Remove all pervious views.
for view in self.scrollView.subviews {
view.removeFromSuperview()
}
for <YOUR NO OF TEXTFIELDS CONDITION> {
let textField : UITextField! = UITextField()
textField.borderStyle = UITextBorderStyle.RoundedRect
textField.font = UIFont.systemFontOfSize(14)
textField.clearButtonMode = UITextFieldViewMode.WhileEditing
textField.setTranslatesAutoresizingMaskIntoConstraints(false)
textField.delegate = self
self.scrollView.addSubview(textField)
// Left constraint
self.scrollView.addConstraint(NSLayoutConstraint(item: textField, attribute: NSLayoutAttribute.Left, relatedBy: NSLayoutRelation.Equal, toItem: self.scrollView, attribute: NSLayoutAttribute.Left, multiplier: 1.0, constant: 10))
// Right constraint
self.scrollView..addConstraint(NSLayoutConstraint(item: textField, attribute: NSLayoutAttribute.Right, relatedBy: NSLayoutRelation.Equal, toItem: self.scrollView, attribute: NSLayoutAttribute.Right, multiplier: 1.0, constant: -10))
// TOP Horizontal constraint
let metrices = ["width" : self.view.bounds.width]
self.scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[textField(width)]", options: NSLayoutFormatOptions(0), metrics: metrices, views: ["textField" : textField]))
if previousTextField == nil {
// Top vertical constraint
self.scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[textField]", options: NSLayoutFormatOptions(0), metrics: nil, views: ["textField" : textField]))
} else {
// Top constraint to previous view
self.scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[previousTextField]-(10)-[textField]", options: NSLayoutFormatOptions(0), metrics: nil, views: ["textField" : textField, "previousTextField" : previousTextField]))
}
previousTextField = textField
}
if previousTextField != nil {
// This below constraints will increase UIScrollView content size
let constraint1 = NSLayoutConstraint.constraintsWithVisualFormat("H:[previousTextField]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["previousTextField" : previousTextField])
self.scrollView.addConstraints(constraint1)
let constraint2 = NSLayoutConstraint.constraintsWithVisualFormat("V:[previousTextField]|", options: NSLayoutFormatOptions(0), metrics: nil, views: ["previousTextField" : previousTextField])
self.scrollView.addConstraints(constraint2)
}
}
Just make the height constraint of those textFields to zero.
textFieldHeightConstraint.constant = 0;

Set constraint so that firstItem's top equals secondItem's height

I'd like to set a top constraint so that it has a relationship which is equal with another item's height.
I prepare it in interface builder so that item 1's top is equal to item 2's top.
Then, in code (because I don't think it can be done in Interface Builder), I try to set the constraint's secondAttribute property.
This seems logical to me based on a basic understanding of how constraints are composed (of two items, an attribute for each, a relationship type, and a constant), but it does not work:
#IBOutlet var fillTopToContainer: NSLayoutConstraint!
// ...
override func viewDidLoad() {
fillTopToContainer.secondAttribute = NSLayoutAttribute.Height
}
Swift compiler error:
Cannot assign to the result of this expression.
I have fiddled with the constant to make sure that topDistEqualsHeight contains the constraint I expect, and it does. The other values in the constraint are correct for my needs, I only want to change the attribute.
Is this a known limitation, a syntax issue, or a big piece of missing knowledge?
Update
I've also tried this, which throws a runtime error:
var pushTopDown = NSLayoutConstraint(item: self.view,
attribute: NSLayoutAttribute.Height,
relatedBy: NSLayoutRelation.Equal,
toItem: fillRect,
attribute: NSLayoutAttribute.Top,
multiplier: 1,
constant: -10)
self.view.addConstraint(pushTopDown)
This is the layout I'm trying to achieve. It's a scrollview which is exactly two screens tall, the bottom half has a fill color.
#P-double suggested that the fillRect match it's top to the bottom position of a full height object, which would work except you can't set the top of a grandchild relative to its grandparent in IB.
View Hierarchy:
frame (fills the screen, root view)
scrollView (fills the frame. Content size is determined by constraints of inside views)
fillRect (height==frame, bottom==scrollView.bottom, top==?)
Constraint's do not work in the way you are trying to use them, most notably constraints properties are all immutable apart from the constant property.
This pairing of constraints does not work, because one relates to an origin point (y-positon), and one relates to a size dimension. It's not clear what you are trying to achieve, but there will be other ways in which you can achieve your desired layout. If you want the second view to sit below the first (in the y-plane, it doesn't necessarily have to align centre-x positions), why not pin the bottom of the first to the top of the second? If you'd like to post some more details, I'll do my best to help.
EDIT
To achieve you desired layout, you should pin the top of the fillRect to the top of the scrollView, and give the constraints constant the value of the frame height. Such as this
var pushTopDown = NSLayoutConstraint(item: scrollView,
attribute: NSLayoutAttribute.Top,
relatedBy: NSLayoutRelation.Equal,
toItem: fillRect,
attribute: NSLayoutAttribute.Top,
multiplier: 1,
constant: self.view.height)
scrollView.addConstraint(pushTopDown)
Also notice that the constraint is added to the scroll view, not the view controllers view. You'll also want to the make the width of fillRect equal to the scrollViews frame width.
As #Rob points out, you'll need to make sure you haven't already added constraint for the top. Interface builder will complain though if the view is not fully constrained. The trick is to add a top constraint in interface builder, but to mark it as a design time constraint. To do this, select the constraint you want to replace in code, the open the attributes inspector on the right, and tick the 'Remove at build time' option. (See picture) This allows the xib/storyboard to compile without error, but doesn't actually add the constraint to the view.
Bottom line, if you try to define a constraint such that the "top" attribute of an item to be equal to the "height" attribute of another, you will receive an error that says:
Invalid pairing of layout attributes
Bottom line, you cannot define constraints between "top" and "height" attributes.
If you want to avoid using spacer views, the other technique to try when vertically spacing views is to set the .CenterYWithinMargins attribute with respect to the superview's .CenterYWithinMargins, applying the appropriate multiple. You can equally space views with judicious use of different multiple values for each item's respective .CenterY attribute.
A couple of observations regarding a few of your attempts: Notably, you cannot mutate secondAttribute of an existing constraint. Only the constant property may be modified after the constraint creation. Also, in your update to your question, you illustrate the attempt to create a new constraint, and you'll obviously want to make sure make sure you remove the old constraint (or define it with a lower priority) before you create a new one.
To illustrate the concept, here is a scroll view with a tinted view that is off screen all created programmatically (it's the most concise way to describe the constraints, methinks):
let scrollView = UIScrollView()
scrollView.setTranslatesAutoresizingMaskIntoConstraints(false)
view.addSubview(scrollView)
let tintedView = UIView()
tintedView.setTranslatesAutoresizingMaskIntoConstraints(false)
tintedView.backgroundColor = UIColor.orangeColor()
scrollView.addSubview(tintedView)
let views = ["scrollView" : scrollView, "tintedView" : tintedView]
// vfl for `frame` of scrollView
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[scrollView]|", options: nil, metrics: nil, views: views))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[scrollView]|", options: nil, metrics: nil, views: views))
// vfl for `contentSize` of scrollView
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[tintedView]|", options: nil, metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[tintedView]|", options: nil, metrics: nil, views: views))
// vfl for `frame` of tintedView
//
// (note, this can be integrated into the above constraints, reducing
// the extraneous VFL, but I implemented them as separate VFL to
// clearly differentiate between settings of the scrollView `contentSize`
// and the tintedView `frame`)
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:[tintedView(==scrollView)]", options: nil, metrics: nil, views: views))
scrollView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:[tintedView(==scrollView)]", options: nil, metrics: nil, views: views))
// offset tinted view
scrollView.addConstraint(NSLayoutConstraint(item: tintedView, attribute: .CenterY, relatedBy: .Equal, toItem: scrollView, attribute: .CenterY, multiplier: 3.0, constant: 0.0))

create UIbutton programmatically with constraints in swift

Thanks in advance for the help.
I want to create a new custom UIButton every time I click an ADD button.
Once I click the add button, I want the add button to slide over and make room for the new button. I want this to happen every time, then create a new row once the buttons fill up the horizontal space of the view
How would I got about doing this?
Note:
I definitely understand how to create a button programmatically, its the constraints/way of getting the buttons to animate sliding over, and spacing correctly that I don't understand
Thank you so much
The image is a quick idea of what I want it to look like. Top row is before I added several buttons, and bottom row is having so many buttons a new row is required
To be able to animate the views you have to set the constant attribute of your constraint variable(the value and direction depends of the attribute, of course), and later you have to call layoutIfNeeded() inside of an UIView animation block.
Code sample:
...
let newButton = UIButton()
containerView.addSubview(newButton)
...
// after adding the button
let horizontalSpace = ButtonWidth + horizontalSpace * 2
let newLeft = lastButtonX + 2 * (buttonWidth + horizontalSpace) + horizontalSpace
let newLeftConstraint = NSLayoutConstraint(item: newButton, attribute: .Left, relatedBy: .Equal, toItem: lastButton, attribute: .Right, multiplier: 0, constant: horizontalInset)
lastButton = newButton
if newLeft + addButtonWidth >= screenWidth {
containerView.layoutIfNeeded()
addButtonLeftConstraint.constant = horizontalSpace
addButtonTopConstraint.constant = buttonRowsNumber * rowHeight + verticalSpace
UIView.animateWithDuration(animationTime) {
containerView.layoutIfNeeded()
}
} else {
containerView.layoutIfNeeded()
addButtonLeftConstraint = newLeft
UIView.animateWithDuration(animationTime) {
containerView.layoutIfNeeded()
}
}
NOTES:
You'll need to keep a var for each constraint you want to animate later on. And depending of you layout behavior, you also need a var to the last button, so you can make the measurements of the positions.
the constant number 0 represents the initial state of the constraint, when it was added and created, so based in this initial state, the view will be moved from it's initial position(starting on the left, or right or whatever initial place you choose).
I suggest you to create the NSLayoutConstraint variables with the class constructor rather than using the visual language, as it generates an array of NSLayoutConstraints and this makes the detections of constraints harder for one specific constraint.
And the final note: I suggest one AL small library to manipulate the constraints more easily trough code, as you can see, constructing NSLayoutConstraints can be very boring and hard to maintain. As you're using Swift, please, take a look at this project: https://github.com/robb/Cartography
I've been using it in my projects, it's really helpful for those situations.
You can add constraint to any view by applying below code.
self.view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-[myLabel]-|", options: nil, metrics: nil, views: viewsDict))
self.view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"H:|-[myButton]-|",
options: nil, metrics: nil, views: viewsDict))
self.view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
"V:|-[myLabel]-[myButton]-|", options: nil, metrics: nil,
views: viewsDict))
There is a good tutorial you can follow from :http://makeapppie.com/2014/07/26/the-swift-swift-tutorial-how-to-use-uiviews-with-auto-layout-programmatically/

Resources