1

I'm trying to add a number of buttons programmatically in a grid, where the number of rows exceeds the screen size, so I want them in a UIScrollView.

My minimal working example is the following:

class ViewController: UIViewController {
    
    //center label and down a bit
    let label = UILabel(frame: CGRect(x: UIScreen.main.bounds.width/2 - 100, y: 50, width: 200, height: 200))
    
    // start scroll view underneath label (why not y: 250?)
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 50, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height))
    
    override func viewDidLoad() {
        super.viewDidLoad()
                
        // add label to view
        self.view.addSubview(label)
        
        // add Scrollview
        self.view.addSubview(scrollView)
        // uncommenting the following also doesn't work
        //scrollView.contentSize = CGSize(width: UIScreen.main.bounds.width, height: 4000)
        
        // add buttons to scroll view in 5x21 grid
        var idx = 1
        for row in 0...20 {
            for col in 0...4 {
                let button = UIButton(frame: CGRect(x: col*50, y: row*50 + 200, width: 50, height: 50))
                button.backgroundColor = .gray
                button.setTitle("\(idx)", for: .normal)
                button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
                
                // add button to scroll view
                scrollView.addSubview(button)
                
                idx += 1
            }
        }
    }
    
    @objc func buttonAction(sender: UIButton!) {
        label.text = sender.title(for: .normal)
    }

}

Unfortunately the scrolling doesn't work. Even when I explicitly set a larger vertical content size (see commented line) //scrollView.contentSize

I'm wondering if it has something to do with setting x and y values for the buttons fixed? But then how else would I align them in a grid?

So, how do I get the button alignment I want while getting a working ScrollView?

Freya W
  • 487
  • 3
  • 11
  • What does 'adding a button to a scroll view' mean? You must be adding a button to something inside a scroll view. – El Tomato Jul 29 '21 at 00:02
  • It’s been a while since I’ve used UIKit, but I believe you want to either 1) use a stack view in the scrollview and add arranged sub views or 2) constrain the subviews to the scrollview bounds. – Jake Jul 29 '21 at 02:20
  • Actually after saying that, are you adding any constraints to the scroll view and it’s contents? – Jake Jul 29 '21 at 02:21
  • I'm using something similar, but horizontally. Thumbnail images. After all, table/stack/collection views are just a "version" of a scroll view. At it's base, you need to define some things - the UIViews, the UIScrollView and (maybe this is the issue) it's content size, and of course, constraints. Do not forget any of this. My guess? You defined the content size too early - `viewDidLoad` never worked for me. Too early in the VC lifecycle. Try `viewDidAppear`, when the frames of everything are established. –  Jul 29 '21 at 03:13
  • @ElTomato really? But just adding it via ```scrollView.addSubview(button)``` does seem to work fine. Except for the scrolling part. – Freya W Jul 29 '21 at 12:39

2 Answers2

3

You really should be using Auto-Layout!!!

By far, the easiest way to add buttons (or any view) to a scroll view - in a grid layout like you're asking - and have it automatically determine the scrolling area is with stack views.

Here's a quick example:

class ViewController: UIViewController {
    
    let label = UILabel()
    let scrollView = UIScrollView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // start with some text in the label
        label.text = "Tap a button"
        
        // center the text in the label
        label.textAlignment = .center
        
        // so we can see the frames
        label.backgroundColor = .yellow
        scrollView.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        // create a vertical stack view to hold the rows of buttons
        let verticalStackView = UIStackView()
        verticalStackView.axis = .vertical

        // we're going to use auto-layout
        label.translatesAutoresizingMaskIntoConstraints = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        verticalStackView.translatesAutoresizingMaskIntoConstraints = false

        // add label to view
        self.view.addSubview(label)
        
        // add Scrollview to view
        self.view.addSubview(scrollView)
        
        // add stack view to scrollView
        scrollView.addSubview(verticalStackView)

        // now let's create the buttons and add them
        var idx = 1
        
        for row in 0...20 {
            // create a "row" stack view
            let rowStack = UIStackView()
            // add it to the vertical stack view
            verticalStackView.addArrangedSubview(rowStack)
            
            for col in 0...4 {
                let button = UIButton()
                button.backgroundColor = .gray
                button.setTitle("\(idx)", for: .normal)
                button.addTarget(self, action: #selector(buttonAction), for: .touchUpInside)
                
                // add button to row stack view
                rowStack.addArrangedSubview(button)
                
                // buttons should be 50x50
                NSLayoutConstraint.activate([
                    button.widthAnchor.constraint(equalToConstant: 50.0),
                    button.heightAnchor.constraint(equalToConstant: 50.0),
                ])
                
                idx += 1
            }
        }

        // finally, let's set our constraints
        
        // respect safe-area
        let safeG = view.safeAreaLayoutGuide

        NSLayoutConstraint.activate([
            
            // constrain label
            //  50-pts from top
            //  80% of the width
            //  centered horizontally
            label.topAnchor.constraint(equalTo: safeG.topAnchor, constant: 50.0),
            label.widthAnchor.constraint(equalTo: safeG.widthAnchor, multiplier: 0.8),
            label.centerXAnchor.constraint(equalTo: safeG.centerXAnchor),
            
            // constrain scrollView
            //  50-pts from bottom of label
            //  Leading and Trailing to safe-area with 8-pts "padding"
            //  Bottom to safe-area with 8-pts "padding"
            scrollView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 50.0),
            scrollView.leadingAnchor.constraint(equalTo: safeG.leadingAnchor, constant: 8.0),
            scrollView.trailingAnchor.constraint(equalTo: safeG.trailingAnchor, constant: -8.0),
            scrollView.bottomAnchor.constraint(equalTo: safeG.bottomAnchor, constant: -8.0),
            
            // constrain vertical stack view to scrollView Content Layout Guide
            //  8-pts all around (so we have a little "padding")
            verticalStackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor, constant: 8.0),
            verticalStackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 8.0),
            verticalStackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -8.0),
            verticalStackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor, constant: -8.0),
            
        ])

    }
    
    @objc func buttonAction(sender: UIButton!) {
        label.text = sender.title(for: .normal)
    }
    
}

and this is the result:

enter image description here

then we'll scroll down and tap a different button:

enter image description here

If you want spacing between the buttons, you can add something like this at the end of viewDidLoad():

// suppose we want 8-pts spacing between the buttons?
verticalStackView.spacing = 8.0
verticalStackView.arrangedSubviews.forEach { v in
    if let stack = v as? UIStackView {
        stack.spacing = 8.0
    }
}

Now it looks like this:

enter image description here

enter image description here

If you want the grid of buttons centered horizontally, or if you want the spacing (or button sizes) to adjust to fit, there are a few different ways to do that... You didn't describe your ultimate goal in your question, but this should be a good starting point.

DonMag
  • 69,424
  • 5
  • 50
  • 86
0

I did the same thing with a collection view which is a subclass of scrollview and that would fit your need in terms of rows/columns.

In addition, to manage touches for a scrollview you have to implement a UIScrollViewDelegate for the scrollview, in particular to distinguish between touching and scrolling otherwise you will possibly end up with unpredictable behaviour.

  • I think the last part might actually be my issue. When deployed on my iPhone I just found out that the scroll is actually implemented, just in the simulator it would only work when clicking and scrolling in a very specific way (that I can't reproduce...) Could you elaborate on the last part? – Freya W Jul 29 '21 at 12:37
  • You should rely on the deployed app only, the simulator does not emulate perfectly. Second,look into https://developer.apple.com/documentation/uikit/uiscrollview there is a managing touches section with a series of methods to handle scrolling vs touching inner views and how the events a passed to subviews. I do not believe that you can avoid this part to has a consistent and predictable behaviour – christophe de poly Jul 29 '21 at 13:37