My Swift 4 UIScrollView with autolayout constraints is not scrolling
Asked Answered
W

1

4

I have created a small demo playground to get this working before adding the view to my app.

I have a scroll view that is going to contain a number of buttons to scroll through horizontally. I know that these buttons need to go into a container view within the scroll view and have created this as well. I was initially using autolayout constraints to create all of this, but have now tried using constants to ensure the content view is bigger than the scroll view. However, the buttons still will not scroll... have I missed something? Do scroll views not work with auto layout?

I am doing this all programmatically as well on my iPad so solutions with interface builder are unfortunately not an option...

Here is the full code:

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(equalToConstant: 200).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.frame = CGRect(x: 0, y: 0, width: 1080, height: 200)
    }

    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)
            button.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        }
    }

    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 : CGFloat = 0.0
        for btn in buttonArray {
            let theBtn = btn.button
            containerView.addSubview(theBtn)
            theBtn.frame = CGRect(x: startPoint, y: 0, width: 200, height: 200)
            startPoint += 220
        }

    }
}

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

Thank you vacawama! Here is the full (working now) mini project with all of the auto layout constraints:

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
        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(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)
            //button.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
        }
    }

    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
            containerView.widthAnchor.constraint(equalTo: theBtn.widthAnchor, multiplier:CGFloat(buttonArray.count), constant: CGFloat(buttonArray.count * 20)).isActive = true
        }
    }
}

let filterViewController = FilterViewController()
PlaygroundPage.current.liveView = filterViewController
Wagonlit answered 4/8, 2018 at 17:39 Comment(4)
This might me complex as a new programmer. You can go for UICollectionView also to achieve this.Thanatopsis
Thanks vivekDas, I tried to make a collection view as well and was running into problems right out the gate. Do you know of any tutorials that will show me how to create a collection programmatically that I can learn from? The ones I was finding were all using interface builder...Wagonlit
You can go through this url: raywenderlich.com/136159/…Thanatopsis
@JohnClevesy: Please don't put your solution into your question. Please move it into an answer. This way, other people can give you upvotes for your solution.Ashy
C
11

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
Chuckwalla answered 4/8, 2018 at 17:57 Comment(8)
I misread something sorry. One more thing. About your 1st suggestions about _ Specify an explicit constant width for the containerView that is larger than the scrollView's width_ don't you also need to specify the scrollView's contentSize?Indiscrete
so when do you need to specify the contentSize would be a third way of doing it?Indiscrete
@Honey, you set contentSize if you aren't using Auto Layout, but the entire answer is about using Auto Layout to make it work.Chuckwalla
This answer is being discussed on meta.Meridithmeriel
It took a few minutes to fully grasp that after adding each button, you update the startPoint to the trailingAnchor of the last added button. And later use that value for the next button's leadingAnchorIndiscrete
Why is disabling scrolling on a textView an effective way for it to expand to autolayout constraints? Is it because then the scrollView's frame and ContainerView's frame would be made identical ie giving the contentOffset a fixed value of 0 ?Indiscrete
I wrote an answer there. Feedbacks are appreciated.Indiscrete
What happens when scrollview zooms? The contentView stays at the same size.Neopythagoreanism

© 2022 - 2024 — McMap. All rights reserved.