ios UIViewController programmatically positioning buttons not working - ios

I would like to evenly space four buttons across a view. In the storyboard I have positioned the buttons in a portrait view so the spacing is correct. But I did not find the correct constraint settings to make the buttons space themselves evenly for any view width (for portrait iPad or landscape orientations). So, I added the following code snippet that moves the buttons to desired locations using the 1st and 4th buttons as the anchors:
// evenly space the buttons
CGPoint leftPoint = self.button1.center;
CGPoint rightPoint = self.button4.center;
CGFloat width = rightPoint.x - leftPoint.x;
leftPoint.x += width / 3;
rightPoint.x -= width / 3;
self.button2.center = leftPoint;
self.button3.center = rightPoint;
The positioning code is working fine, but my difficulty is finding the best place to make the adjustments. - (void)viewDidAppear:(BOOL)animated
seems to be the best spot. However, if I seque to a different view, when I return to this view the buttons will have reverted to their initial (storybaord constraint) specified positions. The viewDidAppear code will get called again but it does not succeed at moving the buttons. It is as if their positions are locked at that point in time.
I guess my primary question is if there is a way to use constraints to achieve the even spacing I am after. Or secondary question is how to override the auto positioning of those two buttons.

This is a relatively hard thing to do using layout constraints, and it depends on exactly what you want. I have an example here that creates 4 buttons (in code) along with 5 labels that are used as spacers between the buttons. The buttons' sizes are determined by their intrinsic content size, and the spacing among the buttons and between the buttons and the sides of the containing view are all the same.
-(void)viewDidLoad {
[super viewDidLoad];
NSMutableDictionary *viewsDict = [NSMutableDictionary dictionary];
NSArray *titles = #[#"Short",#"Longer",#"Short",#"The Longest"];
for (int i=1; i<5; i++) {
UIButton *b = [UIButton buttonWithType:1];
[b setTitle:titles[i-1] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[viewsDict setObject:b forKey:[NSString stringWithFormat:#"b%d",i]];
}
for (int i=1; i<6; i++) {
UILabel *l = [[UILabel alloc ]init];
[l setTranslatesAutoresizingMaskIntoConstraints:NO];
[viewsDict setObject:l forKey:[NSString stringWithFormat:#"l%d",i]];
}
for (id obj in viewsDict.allKeys)
[self.view addSubview:viewsDict[obj]];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"|[l1][b1][l2(==l1)][b2][l3(==l1)][b3][l4(==l1)][b4][l5(==l1)]|"
options:NSLayoutFormatAlignAllBaseline
metrics:nil
views:viewsDict];
NSArray *constraints2 = [NSLayoutConstraint constraintsWithVisualFormat:#"V:[b1]-|"
options:0
metrics:nil
views:viewsDict];
[self.view addConstraints:constraints];
[self.view addConstraints:constraints2];
}
The spacing of the buttons will automatically adjust when the view size changes, as on a rotation.

The solution I was led to is to programmatically add constraints to the two middle buttons (button2 & button3) that position them horizontally relative to the middle of the view. These two constraints allowed me to completely remove the manual positioning code. The answer to Evenly space multiple views within a container view helped get me on the right track.
- (void)viewDidLoad
{
...
self.button2.translatesAutoresizingMaskIntoConstraints = NO;
self.button3.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.button2 attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:0.667 constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.button3 attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual
toItem:self.view attribute:NSLayoutAttributeCenterX multiplier:1.333 constant:0]];

For almost any case of layout problem use layoutSubviews method, it's the place to do it.

Related

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.

Best way to center some images using iOS autolayout

I am doing this and I am curious whether it is the best way, or a dumb way!
I have a bunch of 40 pixel wide images, each one is like a Scrabble tile. My app wants to display some and center them on the screen. Only it don't know how many there are going to be! Could be between 3 and 10.
So I think best thing is if I count how many, multiple by 40, so I know how many pixels wide the whole thing will be, and then let's pretend it's 280 pixels - I will create a 280 px wide UIView, stick all the tiles in there, and then use Autolayout to center that UIView on the device.
That way if user rotates device, no problem!
Is this the best way? Also I am going to need to let the user drag the tiles out of that UIView and into another place on screen. Will that be possible?
Three approaches leap out at me:
I think your solution of using a container view is perfectly fine. But, you don't have to mess around with determining the size of the images. You can just define the relation between the container and the image views, and it will resize the container to conform to the intrinsic size of the image views (or if you explicitly define the size of the image views, that's fine, too). And you can then center the container (and not give it any explicit width/height constraints):
// create container
UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor clearColor];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:containerView];
// create image views
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"1.png"]];
imageView1.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:imageView1];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"2.png"]];
imageView2.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:imageView2];
NSDictionary *views = NSDictionaryOfVariableBindings(containerView, imageView1, imageView2);
// define the container in relation to the two image views
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[imageView1]-[imageView2]|" options:0 metrics:nil views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[imageView1]-|" options:0 metrics:nil views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[imageView2]-|" options:0 metrics:nil views:views]];
// center the container
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
Another common solution with constraints is to create two extra UIView objects (sometimes called "spacer views"), for which you'll specify a background color of [UIColor clearColor], and put them on the left and right of your image views, and define them to go to the margins of the superview, and define the right view to be the same width of the left view. While I'm sure you're building your constraints as you're going along, if we were going to write the visual format language (VFL) for two imageviews to be centered on the screen, it might look like:
#"H:|[leftView][imageView1]-[imageView2][rightView(==leftView)]|"
Alternatively, you could eliminate the need for the container view or the two spacer views on the left and right by creating NSLayoutAttributeCenterX constraints using constraintWithItem, and specifying multiplier for the various image views so that they're spaced the way you want. While this technique eliminates the need for these two spacer views, I also think it's a little less intuitive.
But it might look like:
[imageViewArray enumerateObjectsUsingBlock:^(UIView *view, NSUInteger idx, BOOL *stop) {
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:view
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:view.superview
attribute:NSLayoutAttributeCenterX
multiplier:2.0 * (idx + 1) / ([imageViewArray count] + 1)
constant:0];
[view.superview addConstraint:constraint];
}];
This admittedly employs a slightly different spacing of the image views, but in some scenarios it's fine.
Personally, I'd lean towards the first approach, but any of these work.
If you have a grid layout your best solution is to use the UICollectionView. This is a highly customizable class that can be configured for almost any grid layout requirements.
I've yet to find a better introduction to what UICollectionView can do than the WWDC 2012 videos:
WWDC 2012 Session 205: Introducing Collection Views by Olivier Gutknecht and Luke Hiesterman
WWDC 2012 Session 219: Advanced Collection Views and Building Custom Layouts by Luke the Hiesterman
A good web based tutorial from Ray Wenderlich is here:
http://www.raywenderlich.com/22324/beginning-uicollectionview-in-ios-6-part-12
By the way, I notice that you asked a second question at the conclusion of your question, namely how to drag the image views out of your container.
Let's assume that you've done the constraints as you've suggested in your question, with the tiles being in a container view that you've centered on your main view (see option 1 of my other answer). You would presumably write a gesture recognizer handler, that would, as you start dragging, remove the tile from the container's list of tiles and then animate the updating of the constraints accordingly:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static CGPoint originalCenter;
if (gesture.state == UIGestureRecognizerStateBegan)
{
// move the gesture.view out of its container, and up to the self.view, so that as the container
// resizes, this view we're dragging doesn't move in the process, too
originalCenter = [self.view convertPoint:gesture.view.center fromView:gesture.view.superview];
[self.view addSubview:gesture.view];
gesture.view.center = originalCenter;
// now update the constraints for the views still left in the container
[self removeContainerTileConstraints];
[self.tiles removeObject:gesture.view];
[self createContainerTileConstraints];
[UIView animateWithDuration:0.5 animations:^{
[self.containerView layoutIfNeeded];
}];
}
CGPoint translate = [gesture translationInView:gesture.view];
gesture.view.center = CGPointMake(originalCenter.x + translate.x, originalCenter.y + translate.y);
if (gesture.state == UIGestureRecognizerStateEnded)
{
// do whatever you want when you drop your tile, presumably changing
// the superview of the tile to be whatever view you dropped it on
// and then adding whatever constraints you need to make sure it's
// placed in the right location.
}
}
This will gracefully animate the tiles (and, invisibly, their container view) to reflect that you dragged a tile out of the container.
Just for context, I'll show you how I created the container and the tiles to be used with the above gesture recognizer handler. Let's say that you had an NSMutableArray, called tiles, of your Scrabble-style tiles that were inside your container. You could then create the container, the tiles, and attach a gesture recognizer to each tile like so:
// create the container
UIView *containerView = [[UIView alloc] init];
containerView.backgroundColor = [UIColor lightGrayColor];
containerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:containerView];
self.containerView = containerView; // save this for future reference
// center the container (change this to place it whereever you want it)
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterX
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterX
multiplier:1.0
constant:0]];
[self.view addConstraint:[NSLayoutConstraint constraintWithItem:containerView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:containerView.superview
attribute:NSLayoutAttributeCenterY
multiplier:1.0
constant:0]];
// create the tiles (in my case, three random images), populating an array of `tiles` that
// will specify which tiles the container will have constraints added
self.tiles = [NSMutableArray array];
NSArray *imageNames = #[#"1.png", #"2.png", #"3.png"];
for (NSString *imageName in imageNames)
{
UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:imageName]];
imageView.translatesAutoresizingMaskIntoConstraints = NO;
[containerView addSubview:imageView];
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[imageView addGestureRecognizer:pan];
imageView.userInteractionEnabled = YES;
[self.tiles addObject:imageView];
}
// add the tile constraints
[self createContainerTileConstraints];
And you'd obviously need these utility methods:
- (void)removeContainerTileConstraints
{
NSMutableArray *constraintsToRemove = [NSMutableArray array];
// build an array of constraints associated with the tiles
for (NSLayoutConstraint *constraint in self.containerView.constraints)
{
if ([self.tiles indexOfObject:constraint.firstItem] != NSNotFound ||
[self.tiles indexOfObject:constraint.secondItem] != NSNotFound)
{
[constraintsToRemove addObject:constraint];
}
}
// now remove them
[self.containerView removeConstraints:constraintsToRemove];
}
- (void)createContainerTileConstraints
{
[self.tiles enumerateObjectsUsingBlock:^(UIView *tile, NSUInteger idx, BOOL *stop) {
// set leading constraint
if (idx == 0)
{
// if first tile, set the leading constraint to its superview
[tile.superview addConstraint:[NSLayoutConstraint constraintWithItem:tile
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:tile.superview
attribute:NSLayoutAttributeLeading
multiplier:1.0
constant:0.0]];
}
else
{
// if not first tile, set the leading constraint to the prior tile
[tile.superview addConstraint:[NSLayoutConstraint constraintWithItem:tile
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self.tiles[idx - 1]
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:10.0]];
}
// set vertical constraints
NSDictionary *views = NSDictionaryOfVariableBindings(tile);
[tile.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[tile]|" options:0 metrics:nil views:views]];
}];
// set the last tile's trailing constraint to its superview
UIView *tile = [self.tiles lastObject];
[tile.superview addConstraint:[NSLayoutConstraint constraintWithItem:tile
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:tile.superview
attribute:NSLayoutAttributeTrailing
multiplier:1.0
constant:0.0]];
}

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).

Arranging UIButtons evenly on 3.5” and 4” display

I have now used a lot of time figuring out - auto layout and how to use it, but haven't found an answer on how to solve this issue.
My issue is that I want to align e.g. 5 vertically placed UIButtons evenly on both iPhone 5 and prior displays, but is there a way to do it with the Interface Builder or is it something which should be done in code. When I want the vertical distance between the buttons to be evenly versus the screen resolution (3.5” and 4” display).
I have tried adding vertical constraints between the buttons, but this only adds a gap in the top or bottom of the layout if 4” display is selected.
I'm of course missing something simple, but can't figure it out.
The only way I've gotten this to work is by putting labels (with no titles, so they're invisible) between the edges and between the buttons, so that the buttons and labels take up all the vertical space on the screen. The buttons have an intrinsic size, but the labels don't, so they expand or contract to fill the space correctly.
-(void)viewDidLoad {
[super viewDidLoad];
NSMutableDictionary *viewsDict = [NSMutableDictionary dictionary];
NSArray *titles = #[#"Button 1",#"Button 2",#"Button 3",#"Button 4",#"Button 5"];
for (int i=1; i<6; i++) {
UIButton *b = [UIButton buttonWithType:1];
[b setTitle:titles[i-1] forState:UIControlStateNormal];
[b setTranslatesAutoresizingMaskIntoConstraints:NO];
[viewsDict setObject:b forKey:[NSString stringWithFormat:#"b%d",i]];
}
for (int i=1; i<7; i++) { // labels for spacing
UILabel *l = [[UILabel alloc ]init];
[l setTranslatesAutoresizingMaskIntoConstraints:NO];
[viewsDict setObject:l forKey:[NSString stringWithFormat:#"l%d",i]];
}
for (id obj in viewsDict.allKeys)
[self.view addSubview:viewsDict[obj]];
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:#"V:|[l1][b1][l2(==l1)][b2][l3(==l1)][b3][l4(==l1)][b4][l5(==l1)][b5][l6(==l1)]|"
options:NSLayoutFormatAlignAllLeading
metrics:nil
views:viewsDict];
NSArray *constraints2 = [NSLayoutConstraint constraintsWithVisualFormat:#"|-100-[b1]"
options:NSLayoutFormatAlignAllLeading
metrics:nil
views:viewsDict];
[self.view addConstraints:constraints];
[self.view addConstraints:constraints2];
}
This aligns all the buttons 100 points from the left edge, with equal spacing between the buttons. This will adjust for screen size as well as rotation.

Resources