Vertically stack an array of buttons using auto layout VFL - ios

I'm learning auto layout and I'd like to setup a set of buttons to be vertically stacked and evenly spaced. I'd also like the buttons pinned to the bottom of the view. What's a good way to setup these constraints with VFL? The button list will be passed in as an array of UIButtons.
NSArray *buttons = [button1, button2, button3, button4, ...]
NSMutableArray *allConstraints = [NSMutableArray array]
UIButton *previousButton;
for (UIButton button in buttons) {
// Buttons take up full width
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button);];
[allConstraints addObjectsFromArray:constraints];
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button);];
[allConstraints addObjectsFromArray:constraints];
if (!previousButton) {
NSDictionary *metrics = #{#"padding" : #(10)};
// Make buttons height
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[-(padding)-previousButton(==button)]"
options:0
metrics:metrics
views:NSDictionaryOfVariableBindings(previousButton, button)];
[allConstraints addObjectsFromArray:constraints];
}
previousButton = button;
}
[self.view addConstraints:allConstraints]
This doesn't achieve what I need as the buttons don't get pinned to the bottom of the view.

I would do it in a somewhat different way. Rather than building up the constraints inside a loop, I would build the format string in a loop.
-(void)addButtonsWithConstraints:(NSArray *) buttons {
NSMutableDictionary *views = [NSMutableDictionary new];
for (int i = 0; i<buttons.count; i++) {
[buttons[i] setTranslatesAutoresizingMaskIntoConstraints: NO];
[self.view addSubview:buttons[i]];
[views setObject:buttons[i] forKey:[NSString stringWithFormat:#"button%d",i]];
}
NSMutableString *formatString = [#"V:" mutableCopy];
for (int i = 0; i<buttons.count-1; i++) {
[formatString appendFormat:#"[button%d]-10-", i];
}
[formatString appendFormat:#"[button%lu]|", buttons.count - 1]; // pins the last button to the bottom of the view
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:formatString options:NSLayoutFormatAlignAllLeft | NSLayoutFormatAlignAllRight metrics:nil views:views]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[button0]|" options:0 metrics:nil views:views]];
}
The last line sets button0 to the full width of the view, and the format options in the previous line make all the buttons align their left and right edges.

Hi this might help you out
NSArray *buttons = #[button1, button2, button3, button4, button5];
NSMutableArray *allConstraints = [NSMutableArray array];
UIButton *previousButton;
for (UIButton *button in buttons) {
[button setTranslatesAutoresizingMaskIntoConstraints:NO];
// Buttons take up full width
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"H:|[button]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(button)];
[allConstraints addObjectsFromArray:constraints];
if (!previousButton) {
constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:|->=10-[button(65)]"
options: 0
metrics:nil
views:NSDictionaryOfVariableBindings(button)];
[allConstraints addObjectsFromArray:constraints];
}
if (previousButton) {
NSDictionary *metrics = #{#"padding" : #(10)};
// Make buttons height
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[previousButton]-(20)-[button(65)]"
options:0
metrics:metrics
views:NSDictionaryOfVariableBindings(previousButton, button)];
[allConstraints addObjectsFromArray:constraints];
}
previousButton = button;
}
NSArray *constraints = [NSLayoutConstraint
constraintsWithVisualFormat:#"V:[previousButton]-10-|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(previousButton)];
[allConstraints addObjectsFromArray:constraints];
[self.view addConstraints:allConstraints]

Related

How to add two views on each other with constraints

I am trying to add 2 views with same X, Y. They have space from the edge using addConstraints:[NSLayoutConstraint constraintsWithVisualFormat...]
I tried the strings "H:|20-[A][B]-20-[C]-20-[D]-20|" and V:|20-[A][B]-20-[C]-20-[D]-20|, and centreX == CenterX and centerY==centerY, but they kept conflicting, thinking that A and B should be next to each other.
A is hidden, and on some button clicked, B is hidden and A is shown.
To add views with same centre X,Y You need to first place one of the view say bView (Bottom view), using constatins
NSArray * bVerticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)];
NSArray * bHorizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)];
This places the bView in the superview with 20 as edge offset.
Now place the tView (top View) with same centre as of bView. Here self is the superview of bView and tView
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
Then pin the edges of tView with desired offset say 40
NSArray * tVerticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-40-[tView]-40-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)];
NSArray * tHorizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-40-[tView]-40-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)];
EDIT :
Here is how do do it.
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
bView = [UIView new];
bView.backgroundColor = [UIColor redColor];
bView.translatesAutoresizingMaskIntoConstraints = NO;
tView = [UIView new];
tView.backgroundColor = [UIColor blackColor];
tView.translatesAutoresizingMaskIntoConstraints = NO;
// Firdt bView is added then tView hence tView is exaclty above the bView.
[self.view addSubview:bView];
[self.view addSubview:tView];
// Edges Constrints for bView
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[bView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(bView)]];
// Edges Constints for tView
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|-20-[tView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-20-[tView]-20-|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(tView)]];
// Centring for bView and tView
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterX metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:[bView]-(<=1)-[tView]" options:NSLayoutFormatAlignAllCenterY metrics:nil views:NSDictionaryOfVariableBindings(bView,tView)]];
}
-(IBAction)toggleViews:(id)sender {
NSArray * subViews = self.view.subviews;
[self.view exchangeSubviewAtIndex:[subViews indexOfObject:tView] withSubviewAtIndex:[subViews indexOfObject:bView]];
}

How to draw a horizontal line using UIView and autolayout?

Here is the code I have...this is kind of a simple question...perhaps I shouldn't do it with UIView...but I just wanna draw a decorative horizontal line to separate content. What I am getting is no error the line just does not draw. If I init it with a frame and turn off autolayout, then I do see the line but that's not how I want to implement it since everything is autolayout.
Below the viewDidLoad calls the other methods to create the UI elements and add the constraints.
- (void)viewDidLoad {
[super viewDidLoad];
// Add main layout
[self.scrollView addSubview:self.profileInfoContainer];
// Add UI elements
[self.profileInfoContainer addSubview:self.email];
[self.profileInfoContainer addSubview:self.userName];
[self.profileInfoContainer addSubview:self.hLineSeperator];
[self.profileInfoContainer addSubview:self.navigationContainer];
// Add Constraints
[self addInfoContainerConst];
}
Below i add the constraints for all ui elements in the profileInfoContainer, including the hLineSeperator
- (void)addInfoContainerConst {
// Add constrains
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_userName, _email, _hLineSeperator, _navigationContainer);
NSDictionary *metrics = #{
#"vTop":#60,
#"vBttm":#5,
#"hLeft":#25,
#"hRight":#25,
#"vMiddle":#5
};
NSArray *constraint_POS_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-vTop-[_userName]-vMiddle-[_email]-vMiddle-[_hLineSeperator]-vMiddle-[_navigationContainer]|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-hLeft-[_userName]-hRight-|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_1 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-hLeft-[_email]-hRight-|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_2 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_hLineSeperator(1)]|"
options:0
metrics:metrics
views:viewsDictionary];
NSArray *constraint_POS_H_3 = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[_navigationContainer]|"
options:0
metrics:metrics
views:viewsDictionary];
[self.profileInfoContainer addConstraints:constraint_POS_V];
[self.profileInfoContainer addConstraints:constraint_POS_H];
[self.profileInfoContainer addConstraints:constraint_POS_H_1];
[self.profileInfoContainer addConstraints:constraint_POS_H_2];
[self.profileInfoContainer addConstraints:constraint_POS_H_3];
}
Below is the method to create the UIView that is used to draw the line
- (UIView*)hLineSeperator {
if (!_hLineSeperator) {
_hLineSeperator = [UIView new];
_hLineSeperator.translatesAutoresizingMaskIntoConstraints = NO;
_hLineSeperator.backgroundColor = [UIColor grayColor];
}
return _hLineSeperator;
}
It is not required to add constraints to subviews (specifically like the ones you want to create) created in code. You can do it something like,
UIView *horizontalSeperatorViewOnTop = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.(yourView).frame.size.width, 0.5f)];
horizontalSeperatorViewTop.backgroundColor = [UIColor lightGrayColor];
[self.(yourView) addSubview: horizontalSeperatorViewOnTop];
I do this in my code all the time for horizontal or vertical separators and it works great!

Layout constraints for dynamically created buttons

I am trying to create a view with dynamically created buttons. I am finding it difficult to set the constraints for inner objects other than first one that's created. Where is the issue?
Create & Add buttons to view
-(void) createButton:(NSString *) btnText isButton:(BOOL) type phraseWidth:(NSInteger) width view:(UIView *) currentView {
if (!type) { // if it's a button then create label & button at same place else only create button
// align left to prev button, align baseline
if (prevX == 5) { // button left aligned to rowView, right align none
UIButton *btnView = [[UIButton alloc] init];
btnView.translatesAutoresizingMaskIntoConstraints=NO;
[currentView addSubview:btnView];
NSDictionary *dictScrollConst = NSDictionaryOfVariableBindings(btnView);
NSString *hConstraint = [NSString stringWithFormat:#"H:|-%f-[btnView]|",prevX];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:hConstraint options:0 metrics:nil views:dictScrollConst]];
NSString *vConstraint = #"V:|[btnView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraint options:0 metrics:nil views:dictScrollConst]];
prevObject = btnView;
}
else { // align new button to previous button
UIButton *btnView = [[UIButton alloc] init];
btnView.translatesAutoresizingMaskIntoConstraints=NO;
[currentView addSubview:btnView];
NSDictionary *dictScrollConst = NSDictionaryOfVariableBindings(prevObject,btnView);
NSString *hConstraint = [NSString stringWithFormat:#"H:[prevObject]-%d-[btnView]",kHorizontalSidePadding];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:hConstraint options:0 metrics:nil views:dictScrollConst]];
NSString *vConstraint = #"V:|[btnView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraint options:0 metrics:nil views:dictScrollConst]];
}
}
}
Doesn't allow constraint to be added with respect to previous button created. Throws up exception:
Impossible to set up layout with view hierarchy unprepared for constraint
There's still way too much code for me to work out what's going on, but this much is obvious:
NSString *vConstraint = #"V:|[btnView]|";
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:vConstraint options:0 metrics:nil views:dictScrollConst]];
[currentView addSubview:btnView];
Those lines are in the wrong order. You cannot add a constraint involving a view (here, btnView) at a time when that view is not in the view hierarchy. (That is exactly what the error message is telling you, though granted it phrases it in rather coy terminology.)
So, add the subview. Then add the constraint that affects it.
What I suggest you do is what I always do: start very simple and work your way up to the full extent of the actual problem. So, I suggest as an exercise that you begin with the second row of your layout, and see if you can do just this simple exercise: given the array of titles #[#"Yellow", #"Purple", #"Blue", #"Red"], can you use it to generate four buttons horizontally?
Here's my code for doing that. Notice how clear and simple it is - ruthlessly logical, spare, and plain. We can always add tweaks later, but this is the kind of simplicity you need to try to maintain and build upon, so that you don't confuse yourself:
NSArray* titles = #[#"Yellow", #"Purple", #"Blue", #"Red"];
UIView* previousButton = nil;
for (NSInteger i = 0; i < 4; i++) {
UIButton* b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:titles[i] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:b];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(100)-[b]"
options:0 metrics:nil views:#{#"b":b}]];
if (i == 0) {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(50)-[b]"
options:0 metrics:nil views:#{#"b":b}]];
} else {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[p]-(20)-[b]"
options:0 metrics:nil views:#{#"b":b, #"p":previousButton}]];
}
previousButton = b;
}
Given this, we see at once one of the things wrong with your code: there is no evidence that you are setting the previous button (your prevObject) on any except the first pass, when of course you need to do it on every pass.
Once we have code that works, we can start to modify it to approach what you are wishing to do. For example, it is now easy to change the hard-coded spacing to use variables like yours instead:
NSArray* titles = #[#"Yellow", #"Purple", #"Blue", #"Red"];
UIView* previousButton = nil;
NSInteger initialX = 5; // *
NSInteger horizSpace = 10; // *
for (NSInteger i = 0; i < 4; i++) {
UIButton* b = [UIButton buttonWithType:UIButtonTypeSystem];
[b setTitle:titles[i] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:b];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(100)-[b]"
options:0 metrics:nil views:#{#"b":b}]];
if (i == 0) {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(initialX)-[b]"
options:0 metrics:#{#"initialX":#(initialX)} views:#{#"b":b}]];
} else {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:#"H:[p]-(horizSpace)-[b]"
options:0 metrics:#{#"horizSpace":#(horizSpace)} views:#{#"b":b, #"p":previousButton}]];
}
previousButton = b;
And so forth. The point is: This is how I "grow my code", starting always with the simple and evolving, making sure it works at every iteration, until I reach the thing I'm really trying to do. Go ye and do likewise!

Does it matter which view constraints are added to?

I find it convenient to collect all constraints for a whole view hierarchy and adding them all to the local root view. Is this discouraged and/or a bad idea versus adding constraints to the appropriate views?
Below is a simplification of a common pattern I use. The views are often more complicated with multiple stored constraints that needs updating as views are added or removed.
- (void)updateViewConstraints
{
UIView *content = ...;
UIView *panel = ...;
UILabel *label = ...;
[self.view addSubview:content];
[content addSubview:panel];
[panel addSubview:label];
NSMutableArray *constraints = [NSMutableArray new];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[content]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(content)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[panel]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(panel)]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:#"|[label]|"
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(label)]];
[self.view addConstraints:constraints]
self.storedConstraints = constraints;
[super updateViewConstraints];
}
- (void)invalidateConstraints
{
[self.view removeConstraints:self.storedConstraints];
[self.view needsUpdateConstraints];
}

Using NSLayoutConstraint Visual Format to set width of items

I'm trying to get a hang of NSLayoutConstraints visual format and I am having a little trouble.
What I want is basically a simple UIView with two labels, one on the left, and one on the right, with heights equal to the height of the container, and the widths equal to 1/2 of the container width. The origin of the right label is aligned to the trailing of the left label.
| LEFT LABEL - RIGHT LABEL|
Anyway, here is my attempt:
self.leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *views = #{ #"leftLabel" : self.leftLabel,
#"rightLabel" : self.rightLabel };
NSDictionary *metrics = #{ #"width" : #(CGRectGetWidth(self.frame) / 2) };
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftLabel(width)]" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-width-[rightLabel(width)]" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
Unfortunately, the width of each label remains 0 after this executes. Any suggestions?
UPDATE
Here's the whole class:
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
self.leftLabel = [UILabel new];
self.rightLabel = [UILabel new];
self.loadingView = [[DDLoadingView alloc] init];
self.rightLabel.backgroundColor = [UIColor redColor];
self.leftLabel.backgroundColor = [UIColor redColor];
self.backgroundColor = [UIColor blackColor];
[self addSubview:self.leftLabel];
[self addSubview:self.rightLabel];
[self configureLayout];
}
return self;
}
- (void)configureLayout
{
self.leftLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.rightLabel.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary *views = #{ #"leftLabel" : self.leftLabel,
#"rightLabel" : self.rightLabel,
#"loadingView" : self.loadingView };
NSDictionary *metrics = #{ #"width" : #(CGRectGetWidth(self.frame) / 2) };
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftLabel]-[rightLabel(==leftLabel)]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
[self setNeedsUpdateConstraints];
[self updateConstraintsIfNeeded];
}
You need to set the constraints to use the widths of the other views:
NSArray *constraints;
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[leftLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[rightLabel]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[leftLabel]-[rightLabel(==leftLabel)]-|" options:0 metrics:metrics views:views];
[self addConstraints:constraints];
The last constraint is saying make those two views the same width while pinning them to the left/right egdes of the superview. See the Equal Widths section of the docs
As discussed in the comments, you need to also make sure that the frame of the superview can accommodate the views themselves. |-[ will use the default insets of 20, using |-2- will give insets of 2. If the intrinsic height of the labels is more than the views height minus these insets your view isn't going to show. So you either need to reduce the insets or increase the height of the container.
If you want more information on AutoLayout I recommend the following series of blog posts by #jrturton - see the top of the article for other posts in the series. He's also got a great category on UIView to make adding constraints easier, I use this daily!

Resources