Adapting Stack Views with Size Classes

I previously showed an example of how to use Stack Views, which are new with iOS 9, to make it easier to user Auto Layout. I also showed how to update the axis of the stack view at runtime to manually switch between horizontal and vertical view alignments.

A better approach would have been to have the stack view automatically adapt to the horizontal and vertical size classes of the device. For example, could we make a horizontal stack view switch to a vertical layout when the width is compact but we have a regular height?

Setting Stack View Properties for a Size Class

To make it quick to play with a stack view I have added a tab bar controller to my previous Stacks Xcode project and embedded another view controller. This time I have a stack view set with a horizontal alignment containing two image views. The stack view is constrained to the layout margins so that it fills the view:

If you select the Stack view and use the Attributes inspector you will notice that each of the Stack view properties has a + on the left:

Clicking on one of these + controls gives you a menu which allows you to customise the property for a combination of horizontal and vertical size classes. You can see the result below when I change the axis to Vertical for a compact width and regular height:

You could follow a similiar approach if, for example, you wanted to change the spacing property when transitioning to a compact width. In theory that is all we need to do to have our stack view adapt to size class changes.

Unfortunately at the time of writing using the second beta of iOS 9 this does not work (fixed in beta 4, see below). Rotating an iPhone from portrait to landscape should switch the stack view from vertical to horizontal as the size class changes from regular to compact height. Note that the stack view does configure correctly based on the size classes at launch but it seems the layout is not getting updated when rotating.

Wait a Minute!

I was surprised when this did not work as expected as I was sure I saw it demonstrated in a WWDC video. In fact session 407 Implementing UI designs in Interface Builder does cover it. However the demo only shows the view adapting to the slide over and split view multitasking modes. I checked my sample code and it does indeed work in that situation as shown in the screen capture below when you transition from regular to compact width on an iPad:

So it does seem there is a problem with the autolayout constraints not being updated in response to a change in view size classes. I will file a radar (#21690620 - update 14 July 2015: closed by Apple as a duplicate of #21375886) and update this post if something changes in a future iOS 9 beta.

Update 25 July 2015: This bug was fixed in iOS 9 beta 4. The following workaround is no longer necessary but I have left it commented in the sample code as an example of how to respond to size changes.

Responding to Size Changes

An alternative approach to size classes is to respond to size changes by implementing viewWillTransition(to:with:) which is called when the view changes size.

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  super.viewWillTransition(to: size, with: coordinator)

After calling super this method calls a private function in the view controller to configure the stack view axis based on the new view size:

private func configureViewForSize(_ size: CGSize) {
  if size.width > size.height {
    stackView.axis = .horizontal
  } else {
    stackView.axis = .vertical

I actually prefer this approach over using size classes as it more neatly captures what I want which is to use a horizontal layout whenever the width of the view is greater than the height. We should also configure the stack view when the view is first loaded:

override func viewDidLoad() {

The stack view adapts to device rotations as shown in the screen shots for a horizontal and vertical iPhone 6 below:

Get the Code

The full updated Xcode project is available in my GitHub repository.