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

Now let’s add some labels by first, declaration of computed property and then the actual usage.

    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:
    stackView.addArrangedSubview(titleLabel)
    stackView.addArrangedSubview(titleLabel)
    stackView.addArrangedSubview(titleLabel)
    stackView.addArrangedSubview(titleLabel)
    stackView.addArrangedSubview(titleLabel)

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

UIStackView Inside UIScrollView swift iOS.

UIStackView Inside UIScrollView swift iOS.

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)
        stackView.addArrangedSubview(simpleView)
        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(titleLabel)
        stackView.addArrangedSubview(titleLabel)
        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

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!

UUIStackView Inside UIScrollView horizontal scroll issue fixed with more content

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

This post was also posted on Medium: https://medium.com/@p.chojnowski/uistackview-inside-uiscrollview-uikit-28ef121a0fee

Thanks for reading!