How to get UIScrollView working with AutoLayout, Programmatically

Without Storyboards, visual AutoLayout constraints, or any other nonsense.

I’m using SnapKit, which makes AutoLayout creation not suck. It makes it a snap, even.

I’m just going to give you the code, and talk about it later.

The Code

The view hierarchy looks like so:

"rootViewController.view" UIView
    +--- "containerView" UIView
      +-- "scrollView" UIScrollView
          +-- "contentView" UIView
              +-- Subview 1 UIView
              +-- Subview 2 UIView
              +-- Subview 3 UIView

Step 1

Set the containerView contraints to whatever you want. In my project, it looks like:

view.addSubview(containerView)
containerView.snp_makeConstraints { make in
  make.top.left.right.equalTo(view)
  make.bottom.equalTo(tabBar.snp_top)
}

Step 2

Pin the scrollView’s edges to the containerView

containerView.addSubview(scrollView)
scrollView.snp_makeConstraints { make in
  make.edges.equalTo(containerView)
}

Step 3

Pin the contentView’s edges to the scrollView:

scrollView.addSubview(contentView)
contentView.snp_makeConstraints { make in
  make.edges.equalTo(scrollView)
}

Step 4

Finally, for all of your subviews, The subviews must have defined widths and heights, but also pinned to the contentView. You can get these widths and heights from a superview, but that superview can’t be the scrollView.

Here’s what this looks like when adding n full-sized subviews to the contentView for use in a paging view controller:

for (i, vc) in vcs.enumerate() {
  addChildViewController(vc)
  scrollView.contentView.addSubview(vc.view)
  vc.view.snp_makeConstraints { (make) -> Void in
    if i == 0 {
      // first one, pin left
      make.left.equalTo(scrollView.contentView)
    } else if i == vcs.count - 1 {
      // last one, pin to previous and contentView's right
      make.left.equalTo(vcs[i - 1].view.snp_right)
      make.right.equalTo(scrollView.contentView)
    } else {
      // nth one, pin to previous
      make.left.equalTo(vcs[i - 1].view.snp_right)
    }
    // pin to the top, bottom of contentView
    make.top.bottom.equalTo(scrollView.contentView)
    // define the width and top/bottom relative
    //    to a _parent_ view of the UIScrollView
    make.width.equalTo(containerView)
    make.height.equalTo(containerView)
  }
}

Notice that the left-most subview is pinned to the left of the contentView and the right-most is pinned to the right of the contentView.

Also note that each subview gets the same constraints that the scrollView has, but that’s just because I want the heights & widths of the subviews to be the exact same size as the scrollView itself.

Why This Works

To size the scroll view’s frame with Auto Layout, constraints must either be explicit regarding the width and height of the scroll view, or the edges of the scroll view must be tied to views outside of its subtree.

There’s lots of writing on the topic (each of those are separate links).

The gist is that by pinning your subviews to contentView and then deriving the width and height from views outside the scrollView, the AutoLayout engine has enough information to define the width and height of the scrollView’s .contentSize.