Using AutoLayout to one label to push another, if its growing - ios

Say i have two labels, close to each ether, and one will maybe grow:
So if the left label will change and grow, i would like the right label to move to the right and give space, but not squeeze, like so:
Normally i just use:
CGFloat width = [self.priceLabel.text sizeWithFont:[UIFont systemFontOfSize:13]].width;
self.myLabel.frame = CGRectMake(self.myLabel.frame.origin.x, self.myLabel.frame.origin.y, width,self.myLabel.frame.size.height);
and move the right label to the end of of the left label,
But i'm using AutoLayout and looking for a way to make it possible
Thanks!!

You can start by trying the visual format:
NSString *visualFormat = #"|-[label1]-[label2]";
NSLayoutFormatOptions options = NSLayoutFormatAlignAllCenterY | NSLayoutFormatDirectionLeftToRight;
NSDictionary *views = NSDictionaryOfVariableBindings(label1, label2);
NSArray *layoutConstraints = [NSLayoutConstraint constraintsWithVisualFormat:visualFormat options:options metrics:nil views:views];
[view addConstraints:layoutConstraints];
If you also want to add a right margin and any available space in the middle you can use
NSString *visualFormat = #"|-[label1]-#1-[label2]-|";
Check the visual format guide for all possible options.

Related

AutoLayout Visual language for mixed V+H

How can view layouts such as this, where the subviews are in vertical and horizontal relationships, be described using the Visual language?
Here is one way to do it. Assumptions: views 1 and 2 have a fixed pixel width, view 3 fills remaining width, fixed margin all round and between views. Views 1 and 2 equal height.
If those are wrong assumptions its pretty straightforward to extend this example.
Swift:
let views = ["view1": view1, "view2": view2, "view3": view3]
let metrics = ["m":12,"w":100]
let format0 = "H:|-(m)-[view1(w)]-(m)-[view3]-(m)-|"
let format1 = "H:|-(m)-[view2(w)]-(m)-[view3]-(m)-|"
let format2 = "V:|-(m)-[view1]-(m)-[view2(==view1)]-(m)-|"
let format3 = "V:|-(m)-[view3]-(m)-|"
for string in [format0,format1,format2,format3] as [String] {
self.view.addConstraints(
NSLayoutConstraint.constraintsWithVisualFormat(
string,
options:nil,
metrics:metrics,
views:views))
}
Objective-C:
NSDictionary* views = NSDictionaryOfVariableBindings(view1,view2,view3);
NSDictionary* metrics = #{#"m":#(12),#"w":#(100)};
NSString* format0 = #"H:|-m-[view1(w)]-m-[view3]-m-|";
NSString* format1 = #"H:|-m-[view2(w)]-m-[view3]-m-|";
NSString* format2 = #"V:|-m-[view1]-m-[view2(==view1)]-m-|";
NSString* format3 = #"V:|-m-[view3]-m-|";
for (NSString* string in #[format0,format1,format2,format3]) {
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:string
options:0
metrics:metrics
views:views]];
}
The views under autolayout control need to have their translatesAutoresizingMaskIntoConstraints property set to NO (it defaults to YES).
In one of your comments you say that the views 'already know their frames'. This sounds a little confused: when using autolayout, views don't set frames, frames are the result of the autolayout equations (the autolayout mechanism sets them).
In any case whether or not you use autolayout, views shouldn't set their own frames, that should be the job of the view's superview context. The superview, or its viewController would make frame decisions, as a frame positions a view with respect to the superview.
It sounds like you may mean that the views already know their sizes, based on their content (in the same way that buttons and labels know their sizes). In this case they can return a size value by overriding -(CGSize) intrinsicContentSize in a UIView subclass. Then you can then omit size metrics from the format strings, simplifying them to:
Swift:
let format0 = "H:|-m-[view1]-m-[view3]-m-|"
let format1 = "H:|-m-[view2]-m-[view3]-m-|"
let format2 = "V:|-m-[view1]-m-[view2]-m-|"
let format3 = "V:|-m-[view3]-m-|"
Objective-C:
NSString* format0 = #"H:|-m-[view1]-m-[view3]-m-|";
NSString* format1 = #"H:|-m-[view2]-m-[view3]-m-|";
NSString* format2 = #"V:|-m-[view1]-m-[view2]-m-|";
NSString* format3 = #"V:|-m-[view3]-m-|";
However if the sizes don't all add up (eg 3*m + view1.height + view2.height != superview.height) something's going to break, and you are losing the advantage of using autolayout to flexibly arrange your views to fill the available space.

Space evenly across a view without margins

I use following function to space a dynamic number of views across a View (with autolayout):
-(NSArray*)spaceViews:(NSArray *)views onAxis:(UILayoutConstraintAxis)axis
{
NSAssert([views count] > 1,#"Can only distribute 2 or more views");
NSLayoutAttribute attributeForView;
NSLayoutAttribute attributeToPin;
switch (axis) {
case UILayoutConstraintAxisHorizontal:
attributeForView = NSLayoutAttributeCenterX;
attributeToPin = NSLayoutAttributeRight;
break;
case UILayoutConstraintAxisVertical:
attributeForView = NSLayoutAttributeCenterY;
attributeToPin = NSLayoutAttributeBottom;
break;
default:
return #[];
}
CGFloat fractionPerView = 1.0 / (CGFloat)([views count] + 1);
NSMutableArray *constraints = [NSMutableArray array];
[views enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop)
{
CGFloat multiplier = fractionPerView * (idx + 1.0);
[constraints addObject:[NSLayoutConstraint constraintWithItem:view
attribute:attributeForView
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:attributeToPin
multiplier:multiplier
constant:0.0]];
}];
[self addConstraints:constraints];
return [constraints copy];
}
(The function is from UIView-Autolayout)
The problem is that it has margins. How could this method be changed in order to evenly space the views without margins?
UPDATE
It seems that the method is not working correctly after all, the margins on the outer items are much higher than at the rest.
As an example I changed the code for the left arm to:
self.leftArm = [UIView autoLayoutView];
self.leftArm.backgroundColor = [UIColor grayColor];
[self.view addSubview:self.leftArm];
[self.leftArm pinAttribute:NSLayoutAttributeRight toAttribute:NSLayoutAttributeLeft ofItem:self.body withConstant:-10.0];
[self.leftArm pinAttribute:NSLayoutAttributeTop toSameAttributeOfItem:self.body withConstant:10.0];
[self.leftArm constrainToSize:CGSizeMake(20.0, 200.0)];
Now the robot looks like this:
Note that at the left arm the views are NOT evenly distributed, the margin on top and at the bottom are much higher. I can reproduce the same behavior horizontally and with less items.
I have successfully removed the margins by aligning the outer views with the sides of the superview and aligning the rest within the space between. But now, due to this problem, I always have bigger spaces between the outer views and the views next to them as between the rest.
Does anyone know how to correct this?
UPDATE 2
I have created a fork and added my function to it, I also extended the robot example so you can see what I mean.
Please take a look.
It looks as though this code is from UIView-Autolayout. The latest version of the library has a new method which lets you stop the spacing from being added to edges:
-(NSArray*)spaceViews:(NSArray*)views
onAxis:(UILayoutConstraintAxis)axis
withSpacing:(CGFloat)spacing
alignmentOptions:(NSLayoutFormatOptions)options
flexibleFirstItem:(BOOL)flexibleFirstItem
applySpacingToEdges:(BOOL)spaceEdges;
Just call this method instead and pass NO to the last argument.
I have extended jrturton's UIView-Autolayout (based on this answer) and created a pull request.
It is now possible to distribute the views evenly without margins by calling the new function:
-(NSArray*)spaceViews:(NSArray *)views
onAxis:(UILayoutConstraintAxis)axis
withMargin:(BOOL)margin
and setting the margin parameter to NO.
You can already find that version of UIView-Autolayout here.
The brand new robot with evenly spaced views as teeth:

setting constraints in code doesn't produce the expected result

i'm trying to set a constraint in code and don't get the expected result.
I have a UIView container with 3 buttons (sub views) and i'm trying to set one's leading space to be the average of the other two's leading spaces (so it'll be in the middle, horizontally).
The numbers i get seem to be right when compared to the numbers i see on the storyboard when i place the 3 buttons in the position i want.
I'm getting the leading space by their frame's x value (i've double checked that aligmentRectForFrame: gives the same results), and i average them.
I use:
NSLayoutConstraint *twitterConstraint = [NSLayoutConstraint constraintWithItem:middleButton attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeLeading multiplier:1.0 constant:average];
[twitterConstraint setPriority:UILayoutPriorityRequired];
[self.view setTranslatesAutoresizingMaskIntoConstraints:NO];
[NSLayoutConstraint activateConstraints:#[twitterConstraint]];
the basic functionality works i.e. if i put a number instead of the average i see results. I'm getting unexpected results with the current code. specifically, i'm getting the "middle button" on the right hand side of the other 2 buttons.
help!
idan
Expanding on why #Ken Thomases answer in comments works: Auto Layout first goes from subview-up to collect information, and then goes superview-down to set frames. So this code is sampling values of its subview frames, but at the time it is executed (in updateConstraints or updateViewConstraints or somewhere else) those views' frames haven't yet been set to their auto-layout-approved values. Unpredictable results can happen.
Calling -layoutIfNeeded on this view forces the Auto Layout engine to do this for the subviews--actaully do the layout work. So then sampling those subviews can work.
Unfortunately in this method, problems are created both sampling the frames to get information and calling layoutIfNeeded, which duplicates the expensive layout operations. As noted, it requires the buttons to be all the same size. Regardless, it's probably fine for this use case.
The way to set items to be evenly spaced using the native Auto Layout system (and allowing different-sized items) is spacer views. It's inelegant, but necessary. It can be done manually in IB, and here's how to do it with Visual Format Language:
NSMutableDictionary *autoLayoutDict = [[NSMutableDictionary alloc] init];
NSDictionary *setDictionary = #{
#"leftLabel":self.leftLabel,
#"middleLabel":self.middleLabel,
#"rightLabel":self.rightLabel
};
[autoLayoutDict setValuesForKeysWithDictionary:setDictionary];
int numberOfSpacerViewsRequired = 2;
for (int i = 0; i < numberOfSpacerViewsRequired; i++) {
UIView *spacerViewX = [[UIView alloc] init];
spacerViewX.backgroundColor = [UIColor clearColor];
spacerViewX.userInteractionEnabled = NO;
spacerViewX.translatesAutoresizingMaskIntoConstraints = NO;
NSString *spacerViewKey = [NSString stringWithFormat:#"spacerView%i", i];
[autoLayoutDict setObject:spacerViewX forKey:spacerViewKey];
[self.view addSubview:spacerViewX];
}
NSArray *constraintsToAdd = [NSLayoutConstraint constraintsWithVisualFormat:#"H:[leftLabel]-0-[spacerView0(spacerView1)]-0-[middleLabel]-0-[spacerView1(spacerView0)]-0-[rightLabel]"
options:NSLayoutFormatAlignAllCenterY
metrics:0
views:autoLayoutDict];
[self.view addConstraints:constraintsToAdd];

Auto layout - complex constraints

I'm trying to display content in a table cell using auto layout, programmatically. I'd like for the content to display as follows:
[title]
[image] [date]
[long string of text, spanning the width of the table, maximum of two lines]
My code looks like this:
-(NSArray *)constraints {
NSMutableArray * constraints = [[NSMutableArray alloc]init];
NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_titleLabel, _descriptionLabel, _dateLabel, _ratingBubbleView);
NSDictionary *metrics = #{#"padding":#(kPadding)};
NSString *const kVertical = #"V:|-(>=0,<=padding)-[_titleLabel]-(<=padding)-[_ratingBubbleView]-(<=padding)-[_descriptionLabel]-(>=0,<=padding)-|";
NSString *const kVertical2 = #"V:|-(>=0,<=padding)-[_titleLabel]-(<=padding)-[_dateLabel]-(<=padding)-[_descriptionLabel]-(>=0,<=padding)-|";
NSString *const kHorizontalDescriptionLabel = #"H:|-padding-[_descriptionLabel]-padding-|";
NSString *const kHorizontalTitleLabel = #"H:|-padding-[_titleLabel]";
NSString *const kHorizontalDateLabel = #"H:|-padding-[_ratingBubbleView]-padding-[_dateLabel]";
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kVertical options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kVertical2 options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kHorizontalDescriptionLabel options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kHorizontalTitleLabel options:0 metrics:metrics views:viewsDictionary]];
[constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:kHorizontalDateLabel options:0 metrics:metrics views:viewsDictionary]];
return constraints;
}
This is the result:
OK, I'm not going to try a fix your code. I'm just going to create constraints that I would use to achieve your layout. I'll put the thought process in comments.
First get a nice vertical layout going...
// I'm just using standard padding to make it easier to read.
// Also, I'd avoid the variable padding stuff. Just set it to a fixed value.
// i.e. ==padding not (>=0, <=padding). That's confusing to read and ambiguous.
#"V:|-[titleLabel]-[ratingBubbleView]-[descriptionLabel]-|"
Then go through layer by layer adding horizontal constraints...
// constraint the trailing edge too. You never know if you'll get a stupidly
// long title. You want to stop it colliding with the end of the screen.
// use >= here. The label will try to take it's intrinsic content size
// i.e. the smallest size to fit the text. Until it can't and then it will
// break it's content size to keep your >= constraint.
#"|-[titleLabel]->=20-|"
// when adding this you need the option "NSLayoutFormatAlignAllBottom".
#"|-[ratingBubbleView]-[dateLabel]->=20-|"
#"|-[descriptionLabel]-|"
Try not to "over constrain" your view. In your code you are constraining the same views with multiple constraints (like descriptionLabel to the bottom of the superview).
Once they're defined they don't need to be defined again.
Again, with the padding. Just use padding rather than >=padding. Does >=20 mean 20, 21.5, or 320? The inequality is ambiguous when laying out.
Also, In my constraints I have used the layout option to constrain the vertical axis of the date label to the rating view. i.e. "Stay in line vertically with the rating view". Instead of constraining against the title label and stuff... This means I only need to define the position of that line of UI once.

Displaying views with programmatic auto layout

EDIT
I am using programmatic auto layout and this issue seems to be eluding me,
in this class
#interface FooterButtonView : UIView {
...
}
I am trying to line up two views side by side
- (void)setUpViewWithTwoElements:(UIView*)element1 :(UIView*)element2{
element1.translatesAutoresizingMaskIntoConstraints = NO;
element2.translatesAutoresizingMaskIntoConstraints = NO;
NSDictionary* views = #{#"element1":element1, #"element2":element2};
NSDictionary* metrics = #{#"buttonHeight":#30.0};
NSString* horizontalFormatString = #"H:|-[element1]-[element2]-|";
NSString* verticalFormatString = #"V:[element1(buttonHeight)]-|";
[self addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:horizontalFormatString
options: NSLayoutFormatAlignAllTop | NSLayoutFormatAlignAllBottom
metrics:nil
views:views]];
[self addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:verticalFormatString
options:nil
metrics:metrics
views:views]];
}
however neither elements is being displayed.
in init I am adding both subviews and then calling the above function. Both elements descend from UIButton.
Any suggestions are greatly appreciated. Thank you!
You should see something with the code you posted assuming that the init method that calls this code is itself being called (and FooterButtonView is being displayed with a non-zero size). One thing you're missing though is the relative horizontal sizes of the two views. With the code you have, there's no way for the system to know what size each of the elements should be, just that they should take up the whole width minus the standard spacings. If you want the two views to be the same size, then change to this,
NSString* horizontalFormatString = #"H:|-[element1]-[element2(==element1)]-|";

Resources