Auto layout space between elements relative to screen height [duplicate] - ios

I understand the old Struts and Springs method of aligning, sizing and distributing views in Interface Builder. However, I cannot seem to figure out how to evenly distribute views using auto layout with Xcode 5. There was a way to do it using Xcode 4, but that option is gone.
I have 7 buttons arranged in a vertical stack. On a 3.5" layout, it looks great. When I preview the screen in the 4" layout, all of the buttons remain tightly packed and there is a large amount of space below the last button.
I want them to stay the same height, but I want the space between them to be able flex so they can spread out across the screen.
I've been able to get the height of the buttons to flex and fill the space, but that is not my desired behavior. I would like to learn how to use Auto Layout to replace my old Springs behavior, but I can't seem to find any way to do it through Interface Builder.
I'm ok with the top button either being a fixed space from the top edge or a proportional space from the top edge, likewise for the bottom button and the bottom edge. Those are less important to me, I'm good with either.
But I really need to figure out how to evenly distribute the extra space between each of the items in the view.

EDIT Note that in iOS 9 this technique will become unnecessary, because a UIStackView will perform distribution automatically. I'll add another answer explaining how that works.
How to Perform Even Distribution Using Autolayout
The simplest way to do this in Interface Builder alone (rather than constructing constraints in code) is to use "spacer" views:
Position the top and bottom buttons absolutely.
Place spacer views between all the buttons. Use constraints to position them horizontally (centering them horizontally is simplest) and to set their widths.
Make constraints between each button and the spacer view above and below it, with a Constant of 0.
Now select all the spacer views and set their heights to be equal.
The first screen shot shows me setting this up in IB:
I have deliberately not corrected for the "misplaced views" because I want you to see what it looks like while I'm designing the constraints. Here's the result on both a 4 inch and a 3.5 inch screen:
I have left the spacer views black, just to show you how this technique works, but of course in real life you would make them transparent and hence invisible! So the user sees just your buttons, evenly distributed on either height of screen.
The reason for the use of this technique is that although the notion of equality performs the distribution of values you are asking for, constraints can apply equality only between aspects of views; thus we need the extra views (the spacer views) so that we have things we can make equal to other things (here, the heights of the spacer views).
Other Approaches
Obviously, a more flexible approach is to assign the constraints in code. This may sound daunting, but there's a lot of third-party code out there to help you, such as this sort of thing.
For example, if we have a (possibly invisible) superview whose height acts as a boundary to dictate maximum vertical distribution of our four buttons, we can pin their tops to the vertical center of that superview with a constant of 0 but a multiplier of 0.000001, 0.666667, 1.33333, and 2.0 respectively (if we have four buttons); now the buttons will stay vertically distributed even as the superview changes size in response to screen height or whatever. [In Xcode 5.1, it will be possible to set that up in Interface Builder, but in earlier versions of Xcode it is not possible.]

In iOS 9 / Xcode 7 this problem will be trivially solved in IB. Simply select the buttons (or whatever it is you want to distribute vertically) and choose Editor > Embed In > Stack View. Then you simply configure the stack view:
Provide constraints that position and size the stack view itself. For example, pin the four edges of the stack view to the four edges of its superview.
Set the stack view's attributes. In this case we want Vertical axis, Fill alignment, Equal Spacing distribution.
That's all! However, you may be curious about how this works, because it is still possible to do the same thing manually in code. A stack view performs distribution, not by inserting spacer views, but by inserting spacer guides. A guide (a UILayoutGuide) is a lightweight object that behaves like a view for purposes of layout constraints, but is not a view and therefore doesn't have to be made invisible and doesn't carry any of the overhead of a view.
To illustrate, I'll do in code what the stack view is doing. Presume we have four views to distribute vertically. We assign them constraints for everything but their distribution:
They all have absolute height constraints
Their left is pinned to the superview's left, and their right is pinned to the superview's right
The top view's top is pinned to the superview's top, and the bottom view's bottom is pinned to the superview's bottom
Now, presume we have references to the four views as views, an array. Then:
let guides = [UILayoutGuide(), UILayoutGuide(), UILayoutGuide()]
for guide in guides {
self.view.addLayoutGuide(guide)
}
NSLayoutConstraint.activateConstraints([
// guide heights are equal
guides[1].heightAnchor.constraintEqualToAnchor(guides[0].heightAnchor),
guides[2].heightAnchor.constraintEqualToAnchor(guides[0].heightAnchor),
// guide widths are arbitrary, let's say 10
guides[0].widthAnchor.constraintEqualToConstant(10),
guides[1].widthAnchor.constraintEqualToConstant(10),
guides[2].widthAnchor.constraintEqualToConstant(10),
// guide left is arbitrary, let's say superview margin
guides[0].leftAnchor.constraintEqualToAnchor(self.view.leftAnchor),
guides[1].leftAnchor.constraintEqualToAnchor(self.view.leftAnchor),
guides[2].leftAnchor.constraintEqualToAnchor(self.view.leftAnchor),
// bottom of each view is top of following guide
views[0].bottomAnchor.constraintEqualToAnchor(guides[0].topAnchor),
views[1].bottomAnchor.constraintEqualToAnchor(guides[1].topAnchor),
views[2].bottomAnchor.constraintEqualToAnchor(guides[2].topAnchor),
// top of each view is bottom of preceding guide
views[1].topAnchor.constraintEqualToAnchor(guides[0].bottomAnchor),
views[2].topAnchor.constraintEqualToAnchor(guides[1].bottomAnchor),
views[3].topAnchor.constraintEqualToAnchor(guides[2].bottomAnchor)
])
(Obviously I could make that code cuter and shorter using loops, but I have deliberately unrolled the loops for clarity, so that you can see the pattern and the technique.)

Related

Auto Layout Not So Auto

I have the most basic set up possible. See pic 1:
Believe it or not this is my first project using AutoLayout, I have created everything prior programatically. This basic set up is literally a UIWebView with 1 custom UIView positioned at the bottom. Previously I was using a tool bar that handled everything for me and had no issues with constraints whatsoever. However, the tool bar created discrepancies for event handling when adding a UILongPressGesture to the subview of the UIBarButtonItem so I decided to convert the tool bar to a UIView (Even inserting a UIView into a tool bar, it naturally converts to a button item) for easier handling. But run-time, the view gets pushed off screen by half of the UIView size (48px) See Pic 2. Then when I add buttons, it just gets worse:
I have reviewed the documents and the support HERE with no results. I've spent about 24 hours in total researching it and learned a lot, so my efforts aren't in vein. I KNOW by 'Adding Missing Constraints', the constraints are just recommendations based on the current set up, they aren't concrete in all cases, so I did try to create my own with control drag after reviewing the documents but my set up concluded with no different results, and exponentially more sloppy. So I decided to include the populated constraints by Xcode below :
UIWebView Constraints
Custom UIView (toolBar) Constraints
Any solid starting point recommendations? Does Intrinsic Size have anything to do with it?
EDIT : WORKING CONSTRAINTS I didn't realize you could simply omit a constraint. It seems the culprit was adding one to the top layout guide.
Just for answerer #Matt :
Constant 0 result : there are small gaps at edges
-16 for leading space/trailing space results as a true toolbar emulation in my case with no outstanding warnings or issues. Thanks
Let's talk about the view at the bottom of your interface and how you would use auto layout to position and size it the way a toolbar would be positioned and sized.
To use auto layout, you need to supply sufficient info to determine both position and size. This view is a subview of the view controller's main view. The main view will be resized depending on the screen, so we want to use auto layout to resize the subview ("toolbar") as well. This is what auto layout is for!
So constrain subview leading edge to the leading edge of the superview, and constrain subview trailing edge to the trailing edge of the superview, both with a constant of 0. Now the right and left edges match the superview!
That takes care of horizontal position and size.
Now let's talk about vertical position. The position should be the bottom. So constrain subview bottom edge to the bottom layout guide of the view controller, again with a constant of 0. Now the bottom of the view is at the bottom!
The only thing we don't know is the top of the subview. This, in our situation, is the same as knowing its height. So give the subview a height constraint, set its constant to a reasonable value like 40, and you're done.

Auto Layout without explicit width constraints and with greater-or-equal trailing space misbehaving

I've got a scroll view contained directly inside the content view of a view controller, at full size in both dimensions. The top, bottom, leading and trailing space constraints from the scroll view to its superview (horizontal) and the layout guides (vertical) are all set to 0. The VC is eventually meant to be nested as a child view controller in one or two places. I'm using a Storyboard.
I've placed a number of elements inside the scroll view and constrained them to it, but I'm seeing various kinds of strange behavior. Below is a screenshot with all the subviews of the scroll view selected to display their constraints. The scroll view's four constraints to the top level view are not visible in it. The view controller has been set to Freeform size, with its top level view (and, accordingly, the scroll view's content view) 616 pts high, guaranteeing that scrolling will be necessary at runtime.
Before analyzing the screenshot, here's a list of things that I'm trying to achieve.
The vertical spacing between elements is set by the designer and fixed. (BTW, none of the vertical constraints, text styles etc. in this wireframe are final yet; the whole image is for illustrative purposes only.)
All the labels (except the topmost one) should start at their intrinsic size, expand up to the width of the scroll view (minus the standard HIG horizontal space of 20 pts on both sides).
Buttons are unlikely to be much bigger than this, but in case of localization surprises, we want them to behave just like the labels. (There's an extra vertical ≥ constraint on "Another Button"; it's irrelevant to this question.)
The web view has a fixed height, and its width should be determined by the width of the scroll view; standard 20 pt horizontal space on both sides.
The text views have a minimum height (67 pts here), but they should expand vertically if the contained text is too big to fit. None of them are editable or scrollable. Like the web view, they're horizontally spaced the standard 20 pts apart from the leading and trailing edges.
As you can see, none of the elements have explicit width constraints. The whole thing relies on the leading and trailing space constraints between the elements and the scroll view. The layout, in my mind, would somewhat gracefully work on hypothetical wider-than-320 pt iPhones of the future without changes to the constraints. It would also work after rotating to landscape orientation (it might look a bit silly, but it would work).
I'll go through the points step by step, referring to the screenshot where necessary.
1: This works, nothing out of the ordinary here.
2: The leading constraints of the labels are all simple Equal 20 pt standard spaces. The trailing constraints are Greater Than or Equal 20 pt standard, ostensibly to allow them to grow to be scrollView.frame.size.width-40 wide, but no wider.
3: Same as 2.
4 and 5: Here's where it gets interesting. The web view and the text views are all listed as Misplaced Views, with IB saying their frames will be different at runtime. The orange dashed borders denoting the correct frames only reach horizontally as far as the longest element with a Greater Than Or Equal trailing constraint; here, it's "A Button With A Long Title", whose right edge is where the dashed border edges end.
Constructing this set of views and their constraints, I expected to have some trouble. I knew it would be tricky to have UITextViews that grow vertically taller than the ≥ 67 height defined here, perhaps only possible through code. Getting the labels and buttons to work as specified above through IB alone seemed a bit iffy, too.
What I didn't expect was the web and text views' reported correct frame being only as wide as the widest label or button. It seems that with this setup, the scroll view won't actually be 320 pts wide, but rather only as wide as necessary to fit the longest element and its spacing, and the web and text views are expected to comply. Given that the scroll view is firmly constrained on all sides to the top level view, which is set to be 320 pts wide, I have no idea why this is. SOMETHING must obviously define the initial width of the scroll view, but why aren't the constraints I've made from the scroll view to the top level view doing that?
Given the specifications above for this set of views, what do I need to change to make it happen?
This case demonstrates the fact that I truly do not properly understand Auto Layout yet, and I hope that the answers will enlighten me about many of its crucial aspects.
With respect to Xcode's warnings about misplaced views, select the view controller in storyboard, tap the "Resolve Auto Layout Issues" button in the lower-right-hand corner of the canvas (it looks like a Tie Fighter from Star Wars), then select "Update All Frames in View Controller". This forces all of your views to reflect their constraints.
Using Auto Layout with UIScrollView is a different animal; so much so that Apple felt it necessary to release a Technical Note on the issue: https://developer.apple.com/library/ios/technotes/tn2154/_index.html
When you connect all those constraints between subviews and the inside walls of a scroll view, the result is probably not what you expect. When you pin a subview to the sides of a scroll view, you are not in fact determining the subview's position relative to the scroll view. Instead, you are determining the scroll view's contentSize. This is weird.
You are using Apple's so-called pure Auto Layout approach from the Technical Note. With this approach, the subviews' constraints must dictate the scroll view's contentSize.
Let's take just one of your subviews and ignore all the rest, say one of the text views. And let's only think about that text view along the horizontal axis. If you wanted the text view to be constrained by the width of the scroll view without any horizontal scrolling, you would need to install a fixed-width constraint on the text view that was exactly the width of the scroll view's bounds minus the spacers. After doing this, the content size width would be the sum of the left spacer, the width of the text view, and the right spacer.
Unfortunately, you cannot install a constraint that establishes a relationship between the width of the text view and the width of the scroll view's bounds. And that's really too bad.
I don't actually recommend installing a fixed-width constraint on the text view. Instead, I would start over and use Apple's "mixed approach" from the Technical Note.
With the mixed approach, the subviews' constraints don't determine the scroll view's contentSize. Instead, you must explicitly set the scroll view's contentSize and the frame of a container view (i.e., a UIView content view).
Let's go back to that UITextView and the horizontal axis. Using the mixed approach, you could leave the constraints for the text view as they are (i.e., no fixed-width constraint). You could explicitly set the width of the scroll view's contentSize and the width of content view's frame as early as viewDidLoad. You could explicitly set these values to self.view.bounds.size.width because your scroll view hugs the sides of the main view.
To implement the mixed approach, you will have to instantiate the content view (UIView) in code and not set its translatesAutoresizingMaskIntoConstraints property to NO. By extension, you'll probably need to create your constraints for all those subviews in code as well (I don't know of any way around this). The visual formatting strings are tedious and repetitive, but they're actually easier to work with than constraints created in IB when configuring complex layouts (your layout is sufficiently complex).
I used the mixed approach to solve a SO challenge here: https://stackoverflow.com/a/21770179/1239263. Unfortunately, the solution hasn't been vetted yet. It's always possible that I'm nuts and I don't know what I'm talking about.

Vertically spread/spaced screen elements using Autolayout and Interface Builder

I have a relatively simple portrait-only UI, laid out in a Storyboard, with items which I want to vertically spread to fill both 3.5 inch and 4 inch screens.
In other words, I want the spacing between the controls to be adjusted so that the UI nicely fills the screen, irrespective of the screen form factor.
This doesn't seem like an unusual thing to want to do, however I just can't get Interface Builder (within Xcode 5) to add the right constraints - I only seem to be able to get it to add fixed vertical space constraints, which do not adjust for different screen sizes.
Does anyone know how to do this without resorting to programmatic UI construction? I've invested a lot of effort in getting the Storyboard-based UI just right.
The solution needs to work on both iOS 6 and 7. Thanks!
How to do this depends on exactly what kind of adjustment you want when the screen size changes. One way to do it to give the top and bottom most views vertical spacing constraints to the top and bottom of the superview, respectively. Add a view, I usually use a UILabel with no text, in between all the views you have stacked vertically, and give them equal heights to one another. Give one of those "spacer" views a fixed height, but edit it so its priority is less than 1000 (which means it's not mandatory that it be satisfied). Then add spacing constraints between each nearest neighbor above and below each "real" view and the "spacers", so that you have all the views from top to bottom connected together by vertical spacing constraints. When the screen size changes, the only thing that can change will be the height of the "spacers", since the priority is less than 1000, and all other constraints are mandatory. My constraints look like this:
The labels each have the standard (8 point) spacing to the "real" views above and below them. The top and bottom views should have whatever spacing you want to the screen edges.
Apple have now posted a document which describes the officially-endorsed approach to solving this problem:
https://developer.apple.com/library/ios/documentation/userexperience/Conceptual/AutolayoutPG/AutoLayoutbyExample/AutoLayoutbyExample.html#//apple_ref/doc/uid/TP40010853-CH5-SW8
Summary of the approach: insert spacer views between your controls, which have equal width/height (as applicable) constraints.

How to center horizontally two labels?

I have pretty straightforward requirements where two labels must be centered horizontally. So, I have selected them and chose Editor->align->center horizontally. Then added top space to container constraint to both of them. I also need the labels to shrink/grow regarding content size. However, IB shows errors and several warnings. I could make the labels shrink/grow just by adding pin between them (horizontal space) but they will not be centered in that case. Here are the screenshots:
here are the errors and warnings:
UPDATE theraven gave an interesting suggestion to use dummy view for centering it horizontally and pinning two labels to it. I have removed all existing constraints, added this dummy view and center X + center Y constraints to it. Then pined two labels to it (added horizontal space constraints). However, I still get a bunch of errors and warnings:
UPDATE2 Just updating the question, but still no valid answer found. #Theraven workaround works for iPhone4, iPhone4S, iPhone5 and iPhone5S, however it's not real centering but rather a workaround. Therefore for iPhone6 and iPhone6 Plus it doesn't work as leading and trailing spaces will be fixed and won't automatically resize for larger width.
What you could do is add both labels to another view, like a container view. Then you need to center this one horizontally and add the necessary constraints.
To add the containing UIView, you can select both labels, go to Editor -> Embed In -> View.
Then you would need to add constraints to make the containing view fit the two labels. So something like this:
First Label (left one):
Leading Space to Superview
Top and bottom Space to Superview
Horizontal spacing to the next Label
Second Label:
Trailing space to superview
Top and Bottom to superview (or align top with the first one)
Then the containing view should resize as to fit both labels. Then all you need to do is add the top offset constraint for this container view and a horizontal alignment it in the parent view.
This way, the containing view will grow as much as it needs to fit both labels and the space between them and will always be centered in the parent view.
I took a screenshot of my test constraints in case it helps you more.
Hope this was what you were looking for.
To solve this use a blank UIView in between your two labels and center it horizontally. Then pin the two labels either side of the centered blank view. It is common convention to use spacer views like this in auto-layout.
I really don't like the idea of adding another view just for sake of estethic.
Another alternative is to horizontally-center the left view, and horizzontally space the right view of an amout X with the left one.
Then to give the horizontally-align contraint of the first view a negative value equal to the first view width plus half the views distance. Or use multipliers as said in a previous comment.
But this only works with fixed width views i guess.
Use centered UIStackView as a container for two labels with a spacing required.
I didn't really understand what you wish to do.
The error you get (in the first screen shot) is that you are missing constraint for the x position of the labels.
For UILabel you must have constraint both for y and for x position regarding to the container view, when you selected them both and chose Editor->align->center horizontally, you just say that label1.center.x = label2.center.x.
You still need to say where they will be in the container view, you added the top space to container, so you do have the y position, but you didn't say where the x position should be.
You said
I have pretty straightforward requirements where two labels must be centered horizontally
But where they should be in respect to their container?
thanks
Using spacer views is the best possible solution I could figure out, even though it looks ugly for the developer. The user wouldn't even know what's going on behind the scenes and once you have the spacer UIView, you can always reuse it.

Centering View between neighbors using autolayout in IOS

I am attempting to layout a screen design using constraints (auto-layout) from interface builder and hope to avoid coding constraints, but I would accept an answer for either case I guess.
I have two subviews in a contained in a top level view. I want to fix vertical distance of the first subview to the top of the screen (I was able to accomplish that - common use case for a vertical space constraint). I want the second subview to float (vertically) in the center of the distance between the bottom of first subview and the bottom of the screen. The idea is that the design responds somewhat to iPhone 3.5" vs 4" dimensions.
I am having trouble defining a constraint or set of constraints that would accomplish this.
I have tried setting inequalities on the vertical spacing constraints between the second subview and the top of it's neighbor (the first subview) and the bottom of the superview, and playing with the priorities of those constraints.
An idea that was suggested in a few other related posts on SO is to use a container that is pinned to the bottom of the screen, as well as the pinned to the bottom edge of the first subview, and then center my second subview in the container. I was hoping to avoid complicating the view hierarchy if possible, but maybe that isn't easily avoided.
Any thoughts or suggestions?
UPDATE
This answer describes how to do this in Xcode 6.0 (and probably works in Xcode 5.0 and 5.1).
ORIGINAL
If you want to do this with auto layout, you have to add at least one spacer view. It doesn't have to be the superview of the vertically-centered view (or any other view), so it has a pretty minor effect on your view hierarchy.
I recommend not trying to set up constraints in Interface Builder in Xcode 4.6.3. It's just too painful. But if you really must, this is doable.
Create a spacer (a plain old UIView) from the bottom edge of the top-hugging view to the bottom edge of superview. Set it to hidden. Give it constraints to the top-hugging view and the left and bottom edges of the root view, and pin its width. My spacer width is 10:
Add your middle view (here, a button). Give it a “Horizontal Center in Container” constraint. Then select both the middle view and the spacer view and give them a “Vertical Centers” constraint:
Note that if you drop the middle view when IB is showing the correct guidelines, it will set these constraints for you.
You can test by enabling resizing for descendents (but not siblings & ancestors) and resizing the root view in IB. The middle view will remain centered between the top-hugging view and the bottom of the superview:

Resources