42

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.

enter image description here

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.

Kenny Wyland
  • 20,844
  • 26
  • 117
  • 229
  • 1
    Actually if you really want to you can go right on using springs and struts. Your storyboard can have auto layout turned off. Or, if you want to use both auto layout and springs-and-struts together, make a nib (_.xib_ file) with a superview containing the buttons, set the nib not to use auto layout, set up the springs and struts; then, in code, load the nib, pull out the superview, and insert it into your interface. - However, I have answered your actual question below, i.e. how to do it with auto layout. – matt Jan 01 '14 at 21:13
  • possible duplicate of [Evenly space multiple views within a container view](http://stackoverflow.com/questions/13075415/evenly-space-multiple-views-within-a-container-view) – Rivera Apr 30 '15 at 20:57

2 Answers2

84

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:

  1. Position the top and bottom buttons absolutely.

  2. Place spacer views between all the buttons. Use constraints to position them horizontally (centering them horizontally is simplest) and to set their widths.

  3. Make constraints between each button and the spacer view above and below it, with a Constant of 0.

  4. Now select all the spacer views and set their heights to be equal.

The first screen shot shows me setting this up in IB:

enter image description here

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:

enter image description here

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

Community
  • 1
  • 1
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 14
    I am displeased with both of these options, but that's Apple's fault, not yours. I haven't needed to use spacers since we were making all web pages with tables and 1 px transparent gifs. *sigh* Thank you for the comprehensive answer. – Kenny Wyland Jan 06 '14 at 20:33
  • I generalized a solution to this problem in my custom UITabBar/UITabBarController replacement. Just check the integration tests for my Auto Layout Constraints generation. The project is on Github, [GGTabBar](https://github.com/Goles/GGTabBar) – Goles Jun 08 '14 at 18:47
  • 3
    Ugh, agreed. Good solution under the limiting AutoLayout circumstances, but geez, it feels like we're back in the 90's laying out websites with tables. UIToolbar has flexible spacer elements which is what's needed here, why can't we have it as a layout element? – George Dec 31 '14 at 00:42
  • 3
    This solution users multipliers and requires no spacers: http://stackoverflow.com/a/25898949/202711 – wuf810 Mar 03 '15 at 09:11
13

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

matt
  • 515,959
  • 87
  • 875
  • 1,141