Programmatic layout rules. Some benefits:

  1. Single source of truth for your UI.
  2. Less context switching from code to interface builder.
  3. Less noise from storyboards, xibs, segues etc.
  4. No XML merge conflicts.
  5. Easier to review and debug.
  6. More flexibility for if-else scenarios, animations etc.

There’s probably more.

UIScrollView is great too. Simple layouts nested in a scroll view are common when you don’t need a table view or collection view and the additional complexity they bring.

Programmatically constraining a scroll view and its contents is simple and concise when you get it right.

Constraining Correctly

Let’s say we want vertical scrolling content in a view controller.

First, we add the scroll view as a subview to our view then constrain the scroll view edges to the parent view edges.

view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true

Note: in some contexts, you will want to pin to the views safe area layout guide.

The scroll view now knows its edges. The scroll view content could scroll beyond those edges both vertically and horizontally.

For content to scroll vertically only we must ensure some part of the scroll views subview hierarchy is:

  1. constrained to all edges of the scroll view
  2. constrained to the width of the scroll view

The most unusual part of this is constraining a subview to the leading and trailing edge of the scroll view as well as its width. But this is necessary. If we only constrain to the leading and trailing edge, the subview can’t figure out its width because the edges of the scroll view are where content could scroll past. Constraining the subviews width fixes that.

Technically we could constrain the leading edge and width of a subview only. It would likely look correct but ends up with this message when debugging the view hierarchy.

Layout Issues: Scrollable content size is ambiguous for UIScrollView.

This message is silenced when you constrain both the edges and the width because the subview knows its width and the scroll view can figure out the scrollable content size from the subviews leading and trailing edges.

It would be the same for top, bottom and height constraints if you were scrolling horizontally.

Adding Subviews Directly

If we add subviews directly we just have to ensure that somewhere amongst our subviews, the constraints fulfil the constraints described above.

subview1.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
subview1.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
subview1.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
subview1.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
subview1.heightAnchor.constraint(equalToConstant: 500).isActive = true

subview2.topAnchor.constraint(equalTo: subview1.bottomAnchor).isActive = true
subview2.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
subview2.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
subview2.heightAnchor.constraint(equalToConstant: 500).isActive = true
subview2.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

In this example subview 1 is constrained to the top, leading, trailing and width anchors of the scroll view. Subview 2’s top is constrained to the bottom of subview 2 and its bottom is constrained to the bottom of the scroll view. This fulfils constraining all edges of the scroll view and its width.

Using a Container View

Adding several subviews to a scroll view can lead to ambiguity as to which constraints are for the benefit of the scroll view and which are determining subview layout only.

We could use a container view instead. A container view is simply a UIView constrained correctly to the scroll view. All subviews are added to and constrained to the container view so that they can forget about the scroll view.

Container views also make applying padding to all subviews simple and consistent.

scrollView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: padding).isActive = true
containerView.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -padding * 2).isActive = true
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -padding).isActive = true
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

Using a Container Stack View

Often vertically scrolling content is aligned and distributed in a grid-like manner. A container stack view is great for this. It can also significantly reduce lines of constraint related code required to layout subviews.

scrollView.addSubview(containerStackView)
containerStackView.axis = .vertical
containerStackView.spacing = 10
containerStackView.translatesAutoresizingMaskIntoConstraints = false
containerStackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerStackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
containerStackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
containerStackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
containerStackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
        
containerStackView.addArrangedSubview(subview1)
containerStackView.addArrangedSubview(subview2)

Pinned Bottom View

A final common use case is having a subview fill the entire view controller view when a device is big enough. But on smaller devices, allowing the content to scroll.

For this, we can use either a container view or container stack view and set the height anchor to be greater than or equal to the view controller view.

containerView.heightAnchor.constraint(greaterThanOrEqualTo: view.heightAnchor)

This means the container view will fit the height of the view controller view unless its subviews are forcing it to grow larger than that height.

Note: you may need to account for insets, padding or the safe area layout guide.

Conclusion

Programmatic constraints for scroll views can be simple and concise. While container views are not necessary they could simplify subsequent code and help avoid unexpected layout issues.

Remember

  • For vertically scrolling, constrain at least one subview to all edges of the scroll plus its width