Search code examples
iosswiftuistackview

How to create a grid of 3xInfinity items in Swift


I am trying to create a grid of 3xInfinity items in Swift with same size items. The items are square buttons that can be aligned up to 3 items wide and infinite on the Y axis. I have created a function that takes a UIStackView with axis vertical and every 3 items I create a new UIStackView with horizontal axis inside of the other one. This works well but only when all the buttons are multiple of 3. Whenever they are not the row makes the buttons fill all the available space.

I would like to know how to create a grid of 3xInfinity items in Swift so that the buttons are always evenly spaced, even if there is an odd number of buttons.

How currently is displayed

How currently is

How it should be displayed

ho it should be displayed

Code

let stackview = UIStackView()
contentView.addSubview(stackview)
stackview.axis = .vertical
stackview.alignment = .fill
stackview.distribution = .fill
stackview.spacing = 10

 for _ in 0...feedButtonRows {
            let hstack = UIStackView()
            hstack.axis = .horizontal
            hstack.alignment = .center
            hstack.distribution = .fillEqually
            hstack.spacing = 16
            hstack.translatesAutoresizingMaskIntoConstraints = false
            hstack.widthAnchor.constraint(equalToConstant: stackview.frame.size.width).isActive = true
            for _ in 0..<3 {
                if(count == buttons.count){
                    
                    break
                }
                let button = FoodButtonComponent(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
                hstack.addArrangedSubview(button)
                button.widthAnchor.constraint(equalToConstant: 100).isActive = true
                button.heightAnchor.constraint(equalToConstant: 100).isActive = true
                button.setup(with: buttons[count])
                count+=1
            }
            stackview.addArrangedSubview(hstack)
        }

Solution

  • If you set your vertical stack view's .alignment = .leading and do not give the horizontal "row" stack views width constraints, the buttons will be left-aligned and won't get stretched.

    You can also simplify your code a bit like this:

        var i: Int = 0
        
        while i < numButtons {
            let hstack = UIStackView()
            hstack.axis = .horizontal
            hstack.spacing = 16
            for _ in 0..<3 {
                if i < numButtons {
                    let button = FoodButtonComponent()
                    button.setTitle("\(i)", for: [])
                    button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
                    button.heightAnchor.constraint(equalTo: button.widthAnchor).isActive = true
                    hstack.addArrangedSubview(button)
                }
                i += 1
            }
            stackview.addArrangedSubview(hstack)
        }
        
    

    Note that when views (labels, buttons, etc) are added as arrangedSubviews of a stack view, UIKit automatically sets .translatesAutoresizingMaskIntoConstraints = false -- so no need to explicitly set that.

    Here's a complete example - change the numButtons at the top to see the layouts:

    class StacksVC: UIViewController {
        
        let numButtons: Int = 7
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .systemBackground
            
            let stackview = UIStackView()
            stackview.axis = .vertical
            // this will keep the 1- & 2-button rows left-aligned
            stackview.alignment = .leading
            stackview.distribution = .fill
            stackview.spacing = 10
            
            var i: Int = 0
            
            while i < numButtons {
                let hstack = UIStackView()
                hstack.axis = .horizontal
                hstack.spacing = 16
                for _ in 0..<3 {
                    if i < numButtons {
                        let button = FoodButtonComponent()
                        button.setTitle("\(i)", for: [])
                        button.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
                        button.heightAnchor.constraint(equalTo: button.widthAnchor).isActive = true
                        hstack.addArrangedSubview(button)
                    }
                    i += 1
                }
                stackview.addArrangedSubview(hstack)
            }
            
            stackview.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(stackview)
            
            let g = view.safeAreaLayoutGuide
            NSLayoutConstraint.activate([
                stackview.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
                stackview.centerXAnchor.constraint(equalTo: g.centerXAnchor),
                // set stackview width to (3 x 100) + (2 x 16)
                //  so if we have only 1 or 2 buttons, they will be "left-aligned"
                stackview.widthAnchor.constraint(equalToConstant: 332.0),
            ])
            
            // let's set the vertical stackview background to light gray
            //  if we want to see the framing
            //stackview.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
    
        }
        
    }
    

    When run:

    enter image description here enter image description here

    enter image description here enter image description here

    enter image description here enter image description here

    enter image description here enter image description here