Auto Layout - make a view's dimensions dependent on another view's - ios

Simple deal: I'd like to make a UIView's width half of its superview's width. Here's my code:
//Display a red rectangle:
UIView *redBox = [[UIView alloc] init];
[redBox setBackgroundColor:[UIColor redColor]];
[redBox setTranslatesAutoresizingMaskIntoConstraints:NO];
[self.view addSubview:redBox];
//Make its height equal to its superview's,
//minus standard spacing on top and bottom:
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[redBox]-|" options:0 metrics:nil views:#{#"redBox": redBox}];
[self.view addConstraints:constraints];
//Make its width half of its superviews's width (the problem):
NSLayoutConstraint *constraint = [NSLayoutConstraint
constraintWithItem:redBox
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeWidth
multiplier:0.5
constant:0.0
];
[self.view addConstraint:constraint];
[self.view setBackgroundColor:[UIColor blueColor]];
This is what I get:
If I set multiplier to 1.0, then the view's width is half of its superview. But why is that?

The problem is probably that your red box view is under-constrained. You've given it a width, but nothing to tell it where it should position itself horizontally. Since it should be half the width of the superview, it could choose to position itself on the left half of the superview. Or the right half. Or the middle. You didn't specify, so the framework just gets to pick something.
In this case, it looks like it's choosing to align the center of the red box with the left edge of the superview. This seems like an odd choice, but again, you didn't specify anything. It can pick whatever it wants.
Add another constraint that will position the view horizontally. That should fix the problem.

Related

UIButton does not scroll when adding to UIScrollView

I have added a UIButton, and a UILabel to a UIScrollView. I added two constraints to the button using Auto Layout, and none to the label. I can see label moving on the screen, however I do not see the button moving. I figure this has something to do with the Auto Layout constraints I added to the button. I would like to see to the button scroll the same way I see the label scrolling around on the window / screen.
Below is how I set everything up:
_welcomeScroller = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
_welcomeScroller.userInteractionEnabled = YES;
_welcomeScroller.scrollEnabled = YES;
_welcomeScroller.showsHorizontalScrollIndicator = YES;
_welcomeScroller.showsVerticalScrollIndicator = YES;
#ifdef DEBUG
[_welcomeScroller setBackgroundColor:[UIColor greenColor]];
#endif
[self.view addSubview:_welcomeScroller];
CGSize welcomeScrollerSize = CGSizeMake(2000, 2000);
[_welcomeScroller setContentSize:welcomeScrollerSize];
// add test label
_test = [[UILabel alloc] initWithFrame:CGRectMake(200, 200, 200, 200)];
[_test setText:#"TEST"];
[_test setFont:[UIFont systemFontOfSize:44]];
[_test setBackgroundColor:[UIColor redColor]];
[_welcomeScroller addSubview:_test];
// add about btn to lower right
_welcomeAbout = [UIButton buttonWithType:UIButtonTypeInfoDark];
[_welcomeAbout addTarget:self action:#selector(showAboutScreen:) forControlEvents:UIControlEventTouchUpInside];
[_welcomeAbout setTranslatesAutoresizingMaskIntoConstraints:NO];
[_welcomeScroller addSubview:_welcomeAbout];
NSLayoutConstraint *pullToBottom = [NSLayoutConstraint constraintWithItem:_welcomeAbout attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:_welcomeScroller.superview attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-10.0];
NSLayoutConstraint *pullToRight = [NSLayoutConstraint constraintWithItem:_welcomeAbout attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:_welcomeScroller.superview attribute:NSLayoutAttributeRight multiplier:1.0 constant:-10];
[_welcomeScroller.superview addConstraints:#[pullToBottom, pullToRight]];
From Apple's technical note:
In general, Auto Layout considers the top, left, bottom, and right
edges of a view to be the visible edges. That is, if you pin a view to
the left edge of its superview, you’re really pinning it to the
minimum x-value of the superview’s bounds. Changing the bounds origin
of the superview does not change the position of the view.
The UIScrollView class scrolls its content by changing the origin of
its bounds. To make this work with Auto Layout, the top, left, bottom,
and right edges within a scroll view now mean the edges of its content
view.
The button isn't scrolling because constraints are set relative to the visible left edge of the scrollview.
Apple's solution suggests:
Create a plain UIView content view for your scroll view that will be
the size you want your content to have. Make it a subview of the
scroll view but let it continue to translate the autoresizing mask
into constraints.
Inside this content view, you can use constraints as you would normally.

Animating Constraint Changes, position animates but height jumps without animation

I have a custom UIView. Within that UIView I have a UILabel with a yellow background containing #"8" called labelWhite. I programmatically create 4 constraints for that UILabel: top, left, width, and height.
When I tap the UIView, I change the constant value of the height and animate the layout over 5 seconds. However, the height of the view immediately jumps to the new value, but the y position also changes and jumps to a new value immediately. Then over the 5 second animation, the y position animates back to where it should have stayed all along.
You can see a video of it happening here: http://inadaydevelopment.com/stackoverflow/Constraints0.mov
What it SHOULD do is just remain at y=0 and shrink vertically. What am I doing wrong?
EDIT:
I just discovered that my animations work exactly as intended if my subviews are UIViews, but as UILabels I get the jump-size + animate-position.
What is going on? Is there a way I can get UILabels to animate their size?
This is the code I use to modify and animate the layout:
self.constraintWhiteHeight.constant = secondaryHeight;
[UIView animateWithDuration:5.0 animations:^{
[self layoutIfNeeded];
}];
This is the code I use to create the constraints:
// ------ Top -----
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self.labelWhite
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:0];
[self addConstraint:constraint];
// ------ Left -----
self.constraintWhiteLeft = [NSLayoutConstraint constraintWithItem:self
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:self.labelWhite
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:0];
// ------ Width -----
NSString *constraintString = [NSString stringWithFormat:#"H:[_labelWhite(==%.0f)]", self.bounds.size.width];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintString
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(_labelWhite)];
NSLog(#"constraints: %#", constraints);
self.constraintWhiteWidth = [constraints objectAtIndex:0];
[self.labelWhite addConstraints:constraints];
// ------ Height -----
constraintString = [NSString stringWithFormat:#"V:[_labelWhite(==%.0f)]", primaryHeight];
constraints = [NSLayoutConstraint constraintsWithVisualFormat:constraintString
options:0
metrics:nil
views:NSDictionaryOfVariableBindings(_labelWhite)];
self.constraintWhiteHeight = [constraints objectAtIndex:0];
[self.labelWhite addConstraints:constraints];
I don't think you're doing anything wrong, this seems to be the way that labels behave when you try to animate their height. I know I've wrestled with this problem in the past, and I can't remember if I've ever found a solution that works by animating the constraints in an animation block. The way I have gotten it to work is to use a timer or CADisplayLink to adjust the height constraint.
-(void)shrinkLabel {
self.constraintWhiteHeight.constant -= 1;
if (self.constraintWhiteHeight.constant >= secondaryHeight)
[self performSelector:#selector(shrinkLabel) withObject:nil afterDelay:.05];
}
After Edit:
Although the timer method works, it doesn't always look smooth, depending on the speed and increments you use. Another way to do this is to use a large UIView (the same size as the yellow label in your movie) with a smaller UILabel inside that has centerX and centerY constraints to the larger view. Then, animate the yellow view as usual with animateWithDuration:animations:
-(void)shrinkLabel {
self.yellowViewHeightCon.constant = secondaryHeight;
[UIView animateWithDuration:5 animations:^{
[self layoutIfNeeded];
}];
}

Vertical list of UILabels and NSLayoutConstraint

I have a vertical list of UILabels:
I want to be able to have all the labels line up with the ":" on the right side and keep the spacing to the left side of the superView (createDate label stay put, and the name and year labels would shift to the right).
Code:
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[nameLabel]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(nameLabel)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[headerView]-[nameLabel]-[createDateLabel]-[yearLabel]" options:NSLayoutFormatAlignAllLeading metrics:nil views:NSDictionaryOfVariableBindings(headerView, nameLabel, createDateLabel, yearLabel)]];
EDIT:
Ok, after implementing some suggestions:
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-(52)-[nameLabel]-[createDateLabel]-[yearLabel]" options:NSLayoutFormatAlignAllTrailing metrics:nil views:NSDictionaryOfVariableBindings(headerView, nameLabel, createDateLabel, yearLabel)]];
gives gets them all right aligned:
I would prefer to keep it pinned to the headerView so if that view changes height, I won't need to recode the pin space. Also, if I pin to headerView, it causes the labels to shift all the way to the right:
So that might just be a losing battle.
I still need to figure out how to pin them to the left and keep the ":" lined up. Right now, I pin createDateLabel because when I'm visually looking at it, I can see its the widest. Is there way I can get it to know which label will be the widest?
You can do this by :
Align trailing edges not leading
Pin leading space from superview to be >= [some const value]. This will make the labels have at least the given spacing from the left edge.
Pin the vertical spacing as you are
If you know which label will be the longest, you can also just pin that elements leading space to superview to your constant.
All of these constraints can be made in interface builder too, so that makes your life slightly easier
How about adding individual constraints to shorter labels:
- (void)viewDidLoad
{
[super viewDidLoad];
UILabel *createDateLabel = [[UILabel alloc] init];
createDateLabel.text = #"Created date:";
createDateLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:createDateLabel];
UILabel *yearLabel = [[UILabel alloc] init];
yearLabel.text = #"Year:";
yearLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:yearLabel];
UILabel *nameLabel = [[UILabel alloc] init];
nameLabel.text = #"Name:";
nameLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:nameLabel];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[createDateLabel]" options:0 metrics:nil views:NSDictionaryOfVariableBindings(createDateLabel)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-40-[nameLabel]-[createDateLabel]-[yearLabel]" options:NSLayoutFormatAlignAllTrailing metrics:nil views:NSDictionaryOfVariableBindings(nameLabel, createDateLabel, yearLabel)]];
// skip these
// [self.view addConstraint:[NSLayoutConstraint constraintWithItem:nameLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:createDateLabel attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0]];
// [self.view addConstraint:[NSLayoutConstraint constraintWithItem:yearLabel attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:createDateLabel attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0]];
}
and the output is this:
I agree it is easier to achieve it in IB, but if you are forced to do it in code..
Good reading is this: Creating Individual Layout Constraints
Make all the labels the same width (set a definite width constraint on all of them) and make their text alignment all be right-aligned.
Here's my rendering. No code - this was set up entirely using constraints in Interface Builder. The longest one ("Create Date:") is adopting its natural width; the others have the same width. All contain right-aligned text, obviously. Other needed constraints are obvious.

ios7 auto layout button resize issue

I am using xcode 5 with storyboards and autolayout turned ON. My layouts of one of my ViewControllers has a UIButton with an image background. The view controller also has 3 buttons anchored to the bottom of the view and some labels above. I laid out my storyboards to a 4" retina display (ie: iphone 5+) and the issue I have is when simulating to a 3.5" display (ie: iphone 4, 4s, etc)
I have pinned the height of the anchored buttons at the bottom to remain constant. What I want is when the app runs on a 3.5" display iphone, that the UIButton with the image will resize smaller (keeping all other labels & buttons same size & spacing). So the aspect ratio would remain the same for that UIButton, it would just get smaller.
I haven't been able to find anything in the auto layout tutorial or others online about how to set up constraints to do this (where the height/width remain proportional).
I think what you really want is as follow:
1.Label's top is 100 from its superView and its bottom is 68 from its superView
2.In 4" display, its size is 200x400 with ratio .5
3.In 3.5" display, its size is 312x624 with ratio .5
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// create search bar
CGRect frame = CGRectMake(60, 100, 200, 400);
_label = [[UILabel alloc] initWithFrame:frame];
_label.backgroundColor = [UIColor blueColor];
[self.view addSubview:_label];
// layout search bar
_label.translatesAutoresizingMaskIntoConstraints = NO;
// height
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:0
constant:400];
// width
[_label addConstraint:constraint];
constraint = [NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:_label
attribute:NSLayoutAttributeHeight
multiplier:0.5
constant:0];
[_label addConstraint:constraint];
// vertical
NSDictionary *views = NSDictionaryOfVariableBindings(_label, self.view);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-100-[_label]-68-|"
options:0
metrics:nil
views:views];
[self.view addConstraints:constraints];
// horizontal
constraint = [NSLayoutConstraint constraintWithItem:_label
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.view
attribute:NSLayoutAttributeCenterX
multiplier:1
constant:0];
[self.view addConstraint:constraint];
}
Checkout the 'Taking Control of Auto Layout in Xcode 5' video from the WWDC 2013 Videos -- Quickly: the top part of the Add New Constraints area is probably what your looking for
I guess preserving aspect ratio is NOT possible in auto layouts via interface builder. #fail.
According to this: http://www.raywenderlich.com/50319/beginning-auto-layout-tutorial-in-ios-7-part-2, at the very end of the tutorial.
So, i guess I need to do this programmatically somehow.

UIView not obeying autolayout constraints in UIScrollView

I am adding a UIView to a UIScrollView and constraining it such that it fills the horizontal space, except for some margins. My visual constraint looks like this:
#"|-16-[theLineView]-16-|"
I have made the view one pixel high so it will appear as a line, and placed it between two text labels:
#"V:[someOtherStuff]-[aTextLabel]-[theLineView]-[anotherLabel]"
However, I am finding that the width of the line is only expanding as far as the width of the longest label above/below it.
Why would this be?
P.S I have read this http://developer.apple.com/library/ios/#technotes/tn2154/_index.html
Code
Here is the entirety of the view controller code from a test project that exhibits this issue on the iPad sim.
- (void)viewDidLoad
{
[super viewDidLoad];
self.scrollView = [[UIScrollView alloc] init];
self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
self.scrollView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.scrollView];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"|[scrollView]|"
options:0
metrics:0
views:#{#"scrollView":self.scrollView}]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[scrollView]|"
options:0
metrics:0
views:#{#"scrollView":self.scrollView}]];
self.line1 = [[UIView alloc] init];
self.line2 = [[UIView alloc] init];
self.label1 = [[UILabel alloc] init];
self.label2 = [[UILabel alloc] init];
self.label3 = [[UILabel alloc] init];
for (UILabel *label in #[self.label1, self.label2, self.label3])
{
label.text = #"I am a label and I am long enough that I can be multiline on an iphone but single on ipad";
}
for (UIView *view in #[self.line1, self.line2, self.label1, self.label2, self.label3])
{
view.translatesAutoresizingMaskIntoConstraints = NO;
view.backgroundColor = [UIColor redColor];
[self.scrollView addSubview:view];
}
//horizontal layout - all views/labels should fill the horizontal space expect for margin
for (UIView *view in #[self.line1, self.line2, self.label1, self.label2, self.label3])
{
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|-16-[view]-16-|"
options:0
metrics:0
views:#{#"view":view}];
[self.scrollView addConstraints:constraints];
}
//vertical layout - stack em up
[self.scrollView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[lab1]-[line1(==1)]-[lab2]-[line2(==1)]-[lab3]-|"
options:0
metrics:0
views:#{#"lab1":self.label1, #"line1":self.line1, #"lab2":self.label2, #"line2":self.line2, #"lab3":self.label3}]];
}
UIScrollView automatically shrinks to fit the views inside it. You need to set the width absolutely somewhere.
Recommended tactic:
Completely fixiate the scrollview inside its parent-view using constraints (leading, trailing, top, bottom).
Create a UIView inside the UIScrollView and put everything you need inside it.
Set the constraints so that the UIView will act as a content-view (this means it is big enough to include all elements). Use intrinsic content-size, shrink-resistance and chaining of elements with constraints a lot. The resulting layout must be well-defined and unique (this means if you were to remove all constraints to the outside, the layout would still work).
Connect the bounds of the UIView with their superview (which is the actual content-view of the UIScrollView, NOT the UIScrollView!).
If you do this in interface-builder (it is possible), you need to re-check your constraints every time you touch something in that scene. And by touch I mean "select" not only "modify".
Found a working solution that should work for your use case, too. See here.
Expanding on number 4 of Patric Schenke's answer; because the content size of scrollView is fluid, pinning an internal view to its edges just doesn't work for determining the width of the view. Your left side pin will work, but both won't. Calculating the width of your view based on the next level container up is the way to go. As long as your self.scrollView is pinned flush to its container(which I call containerView), this line of code will accomplish what you want. Add this line to your for loop for horizontal constraints:
// Pin view's width to match the scrollView container's width
// -32 constant offset for margins
[containerView addConstraint:
[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeWidth
relatedBy:NSLayoutRelationEqual
toItem:containerView
attribute:NSLayoutAttributeWidth
multiplier:1.0f
constant:-32]];
I found a simple constraint-based way to accomplish this (I haven't tested the extent of the brittleness of this solution):
...#"H:|-16-[view]-16-|"... // (your original constraint)
[self.scrollView addConstraint:
[NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:self.scrollView
attribute:NSLayoutAttributeCenterX
multiplier:1.0f
constant:0.0f]];
This stretches view all the way out to the other side of the view. This is probably not ideal for content that scrolls horizontally, but should work vertically.
I realize this is over a year later, but it's simpler for the single dimensional scrolling use case than patric.schenke's answers (which are good and more robust).

Resources