UIStackView inside UIScrollView — UIKit

Piotr Chojnowski
4 min readAug 4, 2019

--

I guess you might be wondering how to auto-layout properly UIStackView inside UIScrollView, since you are reading this post.
The thing about auto-layout trick is to setup auto-layout properly, so the UIScrollView will not show horizontal or vertical scrolls when not needed and it’s content size will be set properly.
In this short tutorial I will show you how to build simple UI with use of:

  • UIStackView
  • UIScrollView
  • Programmatic Auto-Layout

Ok, first things first. We are going to build a version of the app where we want to have a proper horizontal scrolling of contents of the stack view. I assume you created a UIViewController and the view is ready to work on.
If not, you can try out by making a new starter project in Xcode – Single View app.
Ok, first step, let’s add a UIScrollView and UIStackView object declarations by this:

lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
return scrollView
}()
lazy var stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = UIStackView.Distribution.equalSpacing
stackView.spacing = 30
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()

Ok, now let’s build two methods that will create subviews, add them to the hierarchy and setup layout.
We want also the StackView to have a 20 points layout margins on the leading and trailing side, by adding:

stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20).isActive = true

ok, to sum up, this part we should have something like this below.

    private func setupViews() {
scrollView.backgroundColor = .lightGray
view.addSubview(scrollView)

scrollView.addSubview(stackView)
}

private func setupLayout() {
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true

stackView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20).isActive = true
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
}
}

We are adding here stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true to tell the UIScrollView how to calculate it’s content size for vertical scrolling only. See reference provided by Apple: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html

var titleLabel: UILabel {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.text = "UIStackView inside UIScrollView."
label.font = UIFont.systemFont(ofSize: 24, weight: .medium)
label.textColor = .white
label.textAlignment = .center
return label
}
/// Update setupViews() method at the end by adding this:
for _ in 0..<5 {
stackView.addArrangedSubview(titleLabel)
}

Ok, now let’s compile it and run!
Below is what you should get.

UIStackView Inside UIScrollView horizontal scroll issue.

To avoid this issue, we have to provide a new content view and then add our UIStackView into it.

Fixing the issue

First, create a new content view that will be holding the UIStackView

lazy var contentView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()

then, change the setupViews method to reflect adding the contentView into the scrollView and add stackView to contentView as a subview:

private func setupViews() {
scrollView.backgroundColor = .lightGray
view.addSubview(scrollView)

scrollView.addSubview(contentView)

contentView.addSubview(stackView)
for _ in 0..<5 {
stackView.addArrangedSubview(titleLabel)
}
}

Now let’s update the setupLayout method to reflect the change in layouting.

private func setupLayout() {
scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor).isActive = true
scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true

contentView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
contentView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

// because: "Constraints between the height, width, or centers attach to the scroll view’s frame." -
// https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true

stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20).isActive = true
stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20).isActive = true
stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20).isActive = true
stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
}

Now we should get this fixed:

UIStackView Inside UIScrollView horizontal scroll issue fixed.

as you can see the margins now are correct and there is no possible scrolling, because the content does not need that!

Let’s try to add more labels by copying few more lines (just to fill empty space) in setupViews method:

private func setupViews() {
scrollView.backgroundColor = .lightGray
view.addSubview(scrollView)

scrollView.addSubview(contentView)

contentView.addSubview(stackView)

for _ in 0...15 {
stackView.addArrangedSubview(titleLabel)
}
}

and here’s the final result!

UIStackView Inside UIScrollView horizontal scroll issue fixed with more content.

If you would like to see the full and finished code go to my Github repository: https://github.com/piotrchojnowski/StackScrollView

Please visit also my personal blog, since part of article will be there exclusively: http://octotap.com/2019/08/03/uistackview-inside-uiscrollview/

Thanks for reading!

--

--