Scrolling Stack Views

Using a Stack View can save a lot of boilerplate auto layout code but there are times when you might wish that it acted more like a table view and scrolled its contents. The UIStackView class is not a subclass of UIScrollView but there is nothing to stop you embedding a stack view in a scroll view.

When to Use

There are a number of situations where you might consider having a stack view embedded in a scroll view. Two common ones that come to mind:

  • You have content in a stack view that you need to move when the keyboard appears.
  • You have a stack view that needs to adapt to different size classes. For example a stack view that fills a regular size view that you also display in a compact size view.

On the other hand don't use a stack view when a table view makes more sense.

Steps to Create

Consider the example where we have a vertical stack view containing a number of images. As images get added to the stack view I want them to scroll within the content area between the labels at the top of the screen and the tab bar at the bottom (keeping the labels visible):

Vertical Stack View

This view uses a number of stack views. Here is how the scene looks in the storyboard:

Storyboard scene

  • Content Stack View: This vertical stack view contains the label and scroll views. Constraints (not shown below) pin it to the edges of the root view. It uses a fill alignment and fill distribution to stretch the subviews to fill the available space.

  • Label Stack View: This vertical stack view contains three UILabel objects. The stack view uses a center alignment and fill distribution.

  • Scroll View: The scroll view fills the remaining space of the content stack view and contains the image stack view.

  • Image Stack View The image stack view contains our images and acts as the content of the scroll view.

The image stack view must have contraints to the leading/trailing and top/bottom edges of the stack view. An equal width constraint with the scroll view ensures the stack view fills the width of the scroll view.

For reference here are the views and constraints in Interface Builder:

IB Constraints

If you are doing this in code:

imageStackView.leadingAnchor.constraintEqualToAnchor(scrollView.leadingAnchor).active = true
imageStackView.trailingAnchor.constraintEqualToAnchor(scrollView.trailingAnchor).active = true
imageStackView.bottomAnchor.constraintEqualToAnchor(scrollView.bottomAnchor).active = true
imageStackView.topAnchor.constraintEqualToAnchor(scrollView.topAnchor).active = true
imageStackView.widthAnchor.constraintEqualToAnchor(scrollView.widthAnchor).active = true

Tap Gestures

To play with what happens when we add and remove views to the stack view I have added three tap gestures each of which has an action method in the view controller:

  • single finger tap: Add a heart image
  • two finger tap: Add a star image
  • three finger tap: Clear the image stack view

Adding Views to the Stack View

The function that responds to a single tap adds a heart image to the stack view and then scrolls to make the added image visible:

@IBAction func singleTap(sender: UITapGestureRecognizer) {
  let heartImage = UIImage(named: "Heart")
  let heartImageView = UIImageView(image: heartImage)
  imageStackView.addArrangedSubview(heartImageView)
  scrollToEnd(heartImageView)
}

The two finger tap method is similar so I will skip it. Here is the three finger tap action to empty the stack view:

@IBAction func threeFingerTap(sender: UITapGestureRecognizer) {
  let views = imageStackView.arrangedSubviews
  for entry in views {
    imageStackView.removeArrangedSubview(entry)
    entry.removeFromSuperview()
  }
}

Scrolling

Scrolling to the end of the stack view can be a little tricky. At the point when we add the view to the stack view the system has not yet done a layout pass. This means it has not yet recalculated the bounds of our stack view and hence the content size of the scroll view.

Since we know the size of the view we just added we can figure out the new content offset for the scroll view. Here is what I came up with in the end:

private func scrollToEnd(addedView: UIView) {
  let contentViewHeight = scrollView.contentSize.height + addedView.bounds.height + imageStackView.spacing
  let offsetY = contentViewHeight - scrollView.bounds.height
  if (offsetY > 0) {
   scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: offsetY), animated: true)
  }
}

If we have everything setup we should be able to scroll when the stack view grows beyond the visible bounds of the scroll view:

Scrolling Stack View

Sample Code

You can find the full working examples from this post in the Stacks Xcode project in my GitHub Code Examples repository.

Further Reading