UIStackView inside UIScrollView
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.
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:
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!
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!