View with two positions using AutoLayout - ios

I have a list of views that I want to position. I have two states where the views should be positioned differently.
I am new to AutoLayout and I might be accessing this the wrong way - I am open to different ways of doing this.
I have created this to position a list of TabBarItem : UIView for the first state:
NSMutableDictionary* metrics = [#{#"margin": #(TAB_BAR_ITEM_MARGIN)} mutableCopy];
for (int i = 0; i < [self.tabBarItems count]; i++)
{
TabBarItem* item = [self.tabBarItems objectAtIndex:i];
if (i > 0)
{
TabBarItem* lastAddedItem = [self.tabBarItems objectAtIndex:i - 1];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(item, lastAddedItem);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[item]-(==margin)-[lastAddedItem]" options:0 metrics:metrics views:viewsDictionary];
[self.view addConstraints:constraints];
}
else
{
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(item);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-(>=0)-[item]-(==margin)-|" options:0 metrics:metrics views:viewsDictionary];
[self.view addConstraints:constraints];
}
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(item);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(==0)-[item]" options:0 metrics:metrics views:viewsDictionary];
[self.view addConstraints:constraints];
}
self.view is the superview of the TabBarItems.
Now I want to be able to move all the TabBarItems to completely different position (will probably require different constraints) using AutoLayout.
How should I approach this? Can I identify a constraint and remove it to add another?
As mentioned I am new to AutoLayout and really want to do this the right way :-)

I achieved this by having two NSMutableArrays: One for all the constraints needed in the first position and one for all the constraints needed in the other position. By doing that I was able to remove the active constraints from the first list and then add the new ones from the second list.
Not very elegant, but I couldn't find another way to do it.

Related

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!

App crashing while adding constraints programatically

I am using visual format to define constraints. The goal is to place a UIView at the bottom of the super view say self.view with height fixed as 40, and width automatically. I have done this using Storyboard but I am unable to do it programatically. Here is the code what I have written. The app is crashing if i am not giving a fixed width. It is crashing with the following constraint: "H:[redView]|". If I change this to "H:[redView(100)]", it works. I don't want to use self.bounds and get width from there. It should stick from left side of super view, bottom and right side of the view.
Please help!
NSDictionary *viewsDictionary = #{#"redView":self.redView};
NSArray *constraint_H = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[redView(40)]"
options:0
metrics:nil
views:viewsDictionary];
NSArray *constraint_V = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[redView]|"
options:0
metrics:nil
views:viewsDictionary];
// 3. Define the redView Position
NSArray *constraint_POS_V = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[redView]|"
options:0
metrics:nil
views:viewsDictionary];
NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[redView]"
options:0
metrics:nil
views:viewsDictionary];
[self.redView addConstraints:constraint_H];
[self.redView addConstraints:constraint_V];
[self.view addConstraints:constraint_POS_H];
[self.view addConstraints:constraint_POS_V];
You need to add redView to its super view first.
Also, the constraints are to be added to the superview, not redView.
[self.view addSubview:self.redView];
// create your constraints here
[self.view addConstraints:#[constraint_H, constraint_V, constraint_POS_V, constraint_POS_H]];
[self.view updateConstraintsIfNeeded];
In the visual constraints syntax the | refers to the parent view. The way you had that horizontal constraint before, adding it to the redview, it was interpreted as redview had a child, called the same, which obviously did not exist, so it crashed.
The vertical constraint works the way it is because it does not reference the parent view.
You could change your code a bit so that the constraints are more clear:
NSArray *verticalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[redView(40)]|" options:0 metrics:nil
views:viewsDictionary];
NSArray *horizontalConstraints = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|[redView]|" options:0 metrics:nil
views:viewsDictionary];
[self.view addConstraints:horizontalConstraints];
[self.view addConstraints:verticalConstraints];
Just remember you should always add the constraints for an UIView to it's superview.
Changing this line
[self.redView addConstraints:constraint_V];
with
[self.view addConstraints:constraint_V];
Solved myself. Thanks to me..:)
If you want to take the width of the view and apply it to red view, you only need to add a constraint like that:
NSArray *constraint_POS_H = [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[redView]-|"
options:0
metrics:nil
views:viewsDictionary];

Vertically stack an array of buttons using auto layout VFL

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]

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];
}

Resources