How to Use Autolayout to Set Constraints on My Uiscrollview

My Swift 4 UIScrollView with autolayout constraints is not scrolling

You can do this with Auto Layout. The secret is to constrain the edges of the containerView to the edges of the scrollView. It's not intuitive, but constraining the edges of the containerView doesn't set the size, it just makes sure that the content size of the scrollView grows as the containerView grows. By setting constraints for the width of the containerView to a constant that is a larger number than the width of the scrollView, the content will scroll horizontally.

Note: When configuring a scrollView this way, you do not set the contentSize of the scrollView. The contentSize will be computed for you by Auto Layout and it will be equal to the size of the containerView. It is important to make sure that the size of the containerView is fully specified by the constraints.

Here's what I changed to make it work:

containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
scrollView.addSubview(containerView)
//containerView.frame = CGRect(x: 0, y: 0, width: 1080, height: 200)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.topAnchor.constraint(equalTo:scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo:scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo:scrollView.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo:scrollView.bottomAnchor).isActive = true
containerView.heightAnchor.constraint(equalToConstant: 200).isActive = true
containerView.widthAnchor.constraint(equalToConstant: 1080).isActive = true

Why isn't my content scrolling?

For it to scroll, the containerView must be larger than the scrollView. Your error is that you have set the constraints such that the containerView is the same width and height as the scrollView, and that is why your content isn't scrolling.

If you want it to scroll horizontally, the width of the containerView must be larger than the scrollView's width. You can do this in one of two ways:

  1. Specify an explicit constant width for the containerView that is larger than the scrollView's width.

    OR

  2. Chain the subviews of the containerView from left to right with the left most being constained to the leading edge of the containerView. Fully specify the widths of the subviews, and place distance contraints between the subviews. The rightmost subview must have an offset from the trailing edge of the containerView. By doing this, Auto Layout can compute the width of the containerView and set the contentSize of the scrollView.


Mini project: update

This is a version of your mini project which uses a chain of constrained views to define the containerView's width. The key is the final constraint after the for loop in viewDidLoad() which connects the last button's trailingAnchor (aka startPoint) to the containerView's trailingAnchor. This completes the chain of contraints and buttons which connect the leading edge of the containerView with the trailing edge of containerView. With this, Auto Layout is able to compute the width of the containerView and establish the contentSize of the scrollView.

import UIKit
import PlaygroundSupport

class FilterViewController: UIViewController {
var filterView: UIView!
var scrollView: UIScrollView!
var containerView: UIView!

override func loadView() {
filterView = UIView()
view = filterView
view.backgroundColor = #colorLiteral(red: 0.909803926944733, green: 0.47843137383461, blue: 0.643137276172638, alpha: 1.0)

scrollView = UIScrollView()
scrollView.backgroundColor = #colorLiteral(red: 0.474509805440903, green: 0.839215695858002, blue: 0.976470589637756, alpha: 1.0)
view.addSubview(scrollView)
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 40).isActive = true
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
scrollView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.25).isActive = true
scrollView.isScrollEnabled = true

containerView = UIView()
containerView.backgroundColor = #colorLiteral(red: 0.176470592617989, green: 0.498039215803146, blue: 0.756862759590149, alpha: 1.0)
scrollView.addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false

// This is key: connect all four edges of the containerView to
// to the edges of the scrollView
containerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor).isActive = true
containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor).isActive = true
containerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

// Making containerView and scrollView the same height means the
// content will not scroll vertically
containerView.heightAnchor.constraint(equalTo: scrollView.heightAnchor).isActive = true
}

class Buttons {
let button = UIButton()
init(titleText: String) {
button.backgroundColor = #colorLiteral(red: 0.976470589637756, green: 0.850980401039124, blue: 0.549019634723663, alpha: 1.0)
button.setTitle(titleText, for: .normal)
}
}

override func viewDidLoad() {
super.viewDidLoad()

let b1 = Buttons(titleText: "one")
let b2 = Buttons(titleText: "two")
let b3 = Buttons(titleText: "three")
let b4 = Buttons(titleText: "four")
let b5 = Buttons(titleText: "five")
let buttonArray = [b1, b2, b3, b4, b5]
var startPoint = containerView.leadingAnchor
for btn in buttonArray {
let theBtn = btn.button
containerView.addSubview(theBtn)
theBtn.translatesAutoresizingMaskIntoConstraints = false
theBtn.leadingAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
theBtn.topAnchor.constraint(equalTo: containerView.topAnchor).isActive = true
theBtn.bottomAnchor.constraint(equalTo: containerView.bottomAnchor).isActive = true
theBtn.widthAnchor.constraint(equalTo: theBtn.heightAnchor).isActive = true
startPoint = theBtn.trailingAnchor
}
// Complete the chain of constraints
containerView.trailingAnchor.constraint(equalTo: startPoint, constant: 20).isActive = true
}
}

let filterViewController = FilterViewController()
PlaygroundPage.current.liveView = filterViewController

How can I use Autolayout to set constraints on my UIScrollview?

It's hard to see the exact values and setup of your constraints as you've pasted them here, so I'm not sure from looking at your screenshots where you have gone wrong.

In lieu of an explanation of what's wrong in your setup, I've created a basic sample project with a very similar view hierarchy and constraint setup to the one you describe. The horizontal scrolling works as expected in the sample project, which uses the "Pure AutoLayout" approach that Apple describes in the Technical Note.

I also had a lot of trouble originally getting Auto Layout to work with UIScrollView. The key to getting it to work is making sure that all of the items in the scroll view, taken together, have constraints that eventually link to all sides of the scroll view and that contribute to the AutoLayout system being able to determine a contentSize for the scroll view that will be bigger than its frame. It looks like you were trying to do that in your code, but maybe you had some superfluous constraints in there that were making the contentSize too small.

Also of note, as others mentioned, with AutoLayout and UIScrollview, you no longer set the contentSize explicitly. The AutoLayout System calculates the contentSize based on your constraints.

I also found this ebook chapter to be very helpful in making me understand how all this works. Hope all this helps.

UIScrollView setup with AutoLayout Programmatically

You don't need either of these lines - remove them:

self.containerView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).isActive = true
self.containerView.heightAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.heightAnchor).priority = .defaultLow

You have no constraint controlling the height of contentView ... you need to add:

// quizOptionsTableView bottom to contentView bottom with 8-points "padding
self.quizOptionsTableView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true

Couple of suggestions...

  • Respect the Safe Area
  • Stick with .leadingAnchor and .trailingAnchor (right now you're mixing in left/right).
  • Group actions together ... that is, do your subview adding in one place, your constraints all together ... your UI element properties all together.
  • Give your UI elements contrasting background colors to make it easy to see the frames.

Very STRONGLY Recommend: use comments!!!!

Take a look at the way I've edited your code:

class SampleViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

let scrollView = UIScrollView()
let titleLabel = UILabel()
let containerView = UIView()
let contentView = UIView()

let questionImageView = UIImageView()
let questionTitleLabel = UILabel()
let questionNumberLabel = UILabel()
let marksLabel = UILabel()
let quizOptionsTableView = UITableView()

let instructionStackView = UIStackView()

let clockImageView = UIImageView()
let timerLabel = UILabel()


override func viewDidLoad() {
super.viewDidLoad()

[scrollView, containerView, contentView, titleLabel, instructionStackView,
questionImageView, questionTitleLabel, questionNumberLabel,
marksLabel, quizOptionsTableView].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
}

//self.scrollView.showsVerticalScrollIndicator = false

self.view.addSubview(self.scrollView)
self.scrollView.addSubview(self.containerView)

self.containerView.addSubview(self.titleLabel)
self.containerView.addSubview(self.instructionStackView)
self.containerView.addSubview(self.contentView)

self.contentView.addSubview(self.questionImageView)
self.contentView.addSubview(self.questionTitleLabel)
self.contentView.addSubview(self.quizOptionsTableView)

self.questionTitleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)

let verticalStackView = UIStackView(frame: .zero)
verticalStackView.axis = .vertical
verticalStackView.addArrangedSubview(self.questionNumberLabel)
verticalStackView.addArrangedSubview(self.marksLabel)
self.questionNumberLabel.setContentHuggingPriority(.defaultHigh, for: .vertical)

let emptyView = UIView(frame: .zero)
emptyView.widthAnchor.constraint(equalToConstant: 40).isActive = true

let horizantalStackView = UIStackView(frame: .zero)
horizantalStackView.axis = .horizontal
horizantalStackView.addArrangedSubview(emptyView)
horizantalStackView.addArrangedSubview(self.clockImageView)
horizantalStackView.addArrangedSubview(self.timerLabel)
horizantalStackView.spacing = 8

self.instructionStackView.addArrangedSubview(verticalStackView)
self.instructionStackView.addArrangedSubview(horizantalStackView)

let safeGuide = view.safeAreaLayoutGuide
let layoutGuide = scrollView.contentLayoutGuide

NSLayoutConstraint.activate([

// all 4 sides of scrollView to view
self.scrollView.topAnchor.constraint(equalTo: safeGuide.topAnchor),
self.scrollView.leftAnchor.constraint(equalTo: safeGuide.leftAnchor),
self.scrollView.rightAnchor.constraint(equalTo: safeGuide.rightAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: safeGuide.bottomAnchor),

// all 4 sides of containerView to scrollView's Content Layout Guide
self.containerView.topAnchor.constraint(equalTo: layoutGuide.topAnchor),
self.containerView.leadingAnchor.constraint(equalTo: layoutGuide.leadingAnchor),
self.containerView.trailingAnchor.constraint(equalTo: layoutGuide.trailingAnchor),
self.containerView.bottomAnchor.constraint(equalTo: layoutGuide.bottomAnchor),

// containerView Width to scrollView's Frame Layout Guide
self.containerView.widthAnchor.constraint(equalTo: self.scrollView.frameLayoutGuide.widthAnchor),

// NO height constraint for containerView

// titleLabel to top of containerView + 16-points "padding"
self.titleLabel.topAnchor.constraint(equalTo: self.containerView.topAnchor, constant: 16),
// titleLabel to leading/trailing of containerView
self.titleLabel.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor),
self.titleLabel.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor),

// instructionStackView top to titleLabel bottom with 24-points "padding"
self.instructionStackView.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 24),
// instructionStackView to leading/trailing of containerView with 50-points "padding"
self.instructionStackView.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor, constant: 50),
self.instructionStackView.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor, constant: -50),

// clockImageView width and height
self.clockImageView.widthAnchor.constraint(equalToConstant: 46),
self.clockImageView.heightAnchor.constraint(equalToConstant: 46),

// contentView top to instructionStackView bottom + 16-points "padding"
self.contentView.topAnchor.constraint(equalTo: self.instructionStackView.bottomAnchor, constant: 16),
// contentView to leading/trailing of containerView with 50-points "padding"
self.contentView.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor, constant: 50),
self.contentView.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor, constant: -50),
// contentView bottom to containerView bottom with 16-points "padding"
self.contentView.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor, constant: -16),

// questionImageView top/leading/trailing to contentView with 8-points "padding"
self.questionImageView.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 8),
self.questionImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8),
self.questionImageView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -8),
// questionImageView height constant
self.questionImageView.heightAnchor.constraint(equalToConstant: 170),

// questionTitleLabel top to questionImageView bottom + 8-points "padding"
self.questionTitleLabel.topAnchor.constraint(equalTo: self.self.questionImageView.bottomAnchor, constant: 8),
// questionTitleLabel leading/trailing to contentView with 8-points "padding"
self.questionTitleLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8),
self.questionTitleLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -8),

// quizOptionsTableView top to questionTitleLabel + 8-points "padding
self.quizOptionsTableView.topAnchor.constraint(equalTo: self.questionTitleLabel.bottomAnchor, constant: 8),
// quizOptionsTableView leading/trailing to contentView with 8-points "padding"
self.quizOptionsTableView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 8),
self.quizOptionsTableView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -8),
// quizOptionsTableView height constant
self.quizOptionsTableView.heightAnchor.constraint(equalToConstant: 320),

// quizOptionsTableView bottom to contentView bottom with 8-points "padding
self.quizOptionsTableView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8),
])

// UI element properties
titleLabel.numberOfLines = 0
titleLabel.textAlignment = .center
titleLabel.text = "This is the text for the Title Label which should be able to wrap onto multiple lines."

questionTitleLabel.numberOfLines = 0
questionTitleLabel.text = "This is the text for the Question Title Label which should be able to wrap onto multiple lines just like the Title Label."

questionNumberLabel.text = "1"
marksLabel.text = "Marks?"

if let img = UIImage(systemName: "clock.fill") {
clockImageView.image = img
}
if let img = UIImage(systemName: "photo.tv") {
questionImageView.image = img
}

quizOptionsTableView.register(UITableViewCell.self, forCellReuseIdentifier: "c")
quizOptionsTableView.dataSource = self
quizOptionsTableView.delegate = self

// let's give our UI elements some constrasting colors so we can see their frames
view.backgroundColor = .lightGray
scrollView.backgroundColor = .red
containerView.backgroundColor = .systemGreen
contentView.backgroundColor = .systemBlue
titleLabel.backgroundColor = .yellow
questionTitleLabel.backgroundColor = .cyan
marksLabel.backgroundColor = .green
clockImageView.backgroundColor = .systemYellow
questionImageView.backgroundColor = .systemYellow
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 20
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "c", for: indexPath)
c.textLabel?.text = "\(indexPath)"
return c
}

}

Swift: I'm not able to make autolayout constraints work for my UIScrollView

Scrollview scrolls in either direction. So any view added to it would need some additional constraints to let the Scrollview calculate its content size.

You have pinned the container view to scroll view. If you are looking at a vertically scrolling view like a table view then you need to also set the width of the scroll to the container view so that it can calculate the width.

Next for height, its automatically calculated based on the UI elements added in the container view. Ensure that all of the labels have leading, trailing to the container and vertical spacing to each other and top of the container view. This will let the scroll view know the height needed.

    self.scrollView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.label1.translatesAutoresizingMaskIntoConstraints = false
self.label2.translatesAutoresizingMaskIntoConstraints = false
self.label3.translatesAutoresizingMaskIntoConstraints = false

self.contentView.addSubview(self.label1)
self.contentView.addSubview(self.label2)
self.contentView.addSubview(self.label3)

self.scrollView.addSubview(self.contentView)
self.view.addSubview(self.scrollView)

NSLayoutConstraint.activate([
self.scrollView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.scrollView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.scrollView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
self.scrollView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor)
])

NSLayoutConstraint.activate([
self.contentView.topAnchor.constraint(equalTo: self.scrollView.topAnchor),
self.contentView.trailingAnchor.constraint(equalTo: self.scrollView.trailingAnchor),
self.contentView.bottomAnchor.constraint(equalTo: self.scrollView.bottomAnchor),
self.contentView.leadingAnchor.constraint(equalTo: self.scrollView.leadingAnchor),
self.contentView.widthAnchor.constraint(equalTo: self.scrollView.widthAnchor)
])

NSLayoutConstraint.activate([
self.label1.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
self.label2.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
self.label3.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
self.label1.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
self.label2.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
self.label3.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor)
])

NSLayoutConstraint.activate([
self.label1.topAnchor.constraint(equalTo: self.contentView.topAnchor),
self.label2.topAnchor.constraint(equalTo: self.label1.bottomAnchor),
self.label3.topAnchor.constraint(equalTo: self.label2.bottomAnchor),
self.label3.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
])

And when run, it won't complain about any constraint issues either. Same can be done for horizontal scroll.

Sample Image

Getting ScrollView to Work with Autolayout and Storyboard

You have to provide the contentSize for a UIScrollView when laying it out using constraints.

For example:

You can set the width of View to be equal to the scrollView as this will provide the width of the scrollview's content. Then, to provide content height, layout red and yellow views vertically and use them to provide the height of View. This means you'll have to provide initial height for yellow view, or you can use a view that provides an intrinsicContentSize to determine the yellow view height so you don't have to manually change it.

Also I think you have to remove redview top == layout guide top, and add redview top == View top and yellowview bottom == View bottom

Understanding UIView Height When Constraints Are Set with AutoLayout/UIScrollView

1- You need to hook viewB leading , trailing , top and bottom to viewA , plus width of viewB is set = width of main vc's view

2- all constraints inside viewB should be hooked properly from top to bottom

3- instead of repeatedly using .isActive = true consider wrapping all constraints inside NSLayoutConstraint.activate([<#ConstraintsHere#>])

4- May be for your current approach a UITableView is more suitable

UIScrollView with Auto Layout Constraints: Auto Content Size Calculation

I found the solution i was looking for here:

[super viewDidLoad];
UIScrollView* sv = [UIScrollView new];
sv.backgroundColor = [UIColor whiteColor];
sv.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:sv];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[sv]|"
options:0 metrics:nil
views:@{@"sv":sv}]];
[self.view addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[sv]|"
options:0 metrics:nil
views:@{@"sv":sv}]];
UILabel* previousLab = nil;
for (int i=0; i<30; i++) {
UILabel* lab = [UILabel new];
lab.translatesAutoresizingMaskIntoConstraints = NO;
lab.text = [NSString stringWithFormat:@"This is label %i", i+1];
[sv addSubview:lab];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(10)-[lab]"
options:0 metrics:nil
views:@{@"lab":lab}]];
if (!previousLab) { // first one, pin to top
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(10)-[lab]"
options:0 metrics:nil
views:@{@"lab":lab}]];
} else { // all others, pin to previous
[sv addConstraints:
[NSLayoutConstraint
constraintsWithVisualFormat:@"V:[prev]-(10)-[lab]"
options:0 metrics:nil
views:@{@"lab":lab, @"prev":previousLab}]];
}
previousLab = lab;
}
// last one, pin to bottom and right, this dictates content size height
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"V:[lab]-(10)-|"
options:0 metrics:nil
views:@{@"lab":previousLab}]];
[sv addConstraints:
[NSLayoutConstraint constraintsWithVisualFormat:@"H:[lab]-(10)-|"
options:0 metrics:nil
views:@{@"lab":previousLab}]];
// look, Ma, no contentSize!

Using Scroll View with Autolayout Swift

I figured this out with the help of the other answers but I had to make some adjustments to get it work the way I wanted. Here are the steps I took:

  1. Add a Scroll View as a Sub View of the Main View.

  2. Select the Scroll View and uncheck "Constrain to margins" and pin top, left, right, bottom, constraints

  3. Add a UIView as a subview of the Scroll View. Name this view "Content View"

  4. Select the Content View and pin top, left, right, and bottom constraints. Then add a center horizontally constraint.

  5. Next from the Content View to the Main View add equal width and equal height constraints.

  6. Add whatever elements you need inside the Content View. Pin top, left, right, and height constraints to the elements that were just added.

  7. On the bottom most item inside the Content View pin a bottom constraint. Select this constraint and change to "Greater Than or Equal". Change the constant to 20.

The constraints added to the items inside the Content View are very important, especially the bottom constraint added to the last item. They help to determine the content size of the scroll view. Adding the bottom constrain as I described will enable the view to scroll if the content is too large to fit in the screen, and disable scrolling if the content does fit in the screen.



Related Topics



Leave a reply



Submit