skip to Main Content

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)
        }

2

Answers


  1. You can’t have distribution = .fillEqually and then at the same time expect that constraining your view size will have any effect using widthAnchor.constraint(equalToConstant: 100).isActive = true. You must be getting some runtime warnings about constraints being in conflict.

    Also when using constraints remember to disable auto-translation of resizing mask into constraints for in-code-generated views.

    let button = FoodButtonComponent(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
    button.translatesAutoresizingMaskIntoConstraint = false
    

    Anyway, you can still do what you want by simply adding an empty view instead of breaking when index goes past 3. You can actually create a standalone method to layout your views. Check out this implementation:

    func layoutViewsVertically(_ views: [UIView], numberOfItemsPerRow: Int, horizontalSpacing: CGFloat = 0, verticalSpacing: CGFloat = 0) -> UIStackView {
        let verticalStackView = UIStackView()
        verticalStackView.axis = .vertical
        verticalStackView.distribution = .fillEqually
        verticalStackView.spacing = verticalSpacing
        
        var viewsToLayout = views
        while viewsToLayout.isEmpty == false {
            let horizontalStackView = UIStackView()
            horizontalStackView.spacing = horizontalSpacing
            horizontalStackView.translatesAutoresizingMaskIntoConstraints = false
            horizontalStackView.axis = .horizontal
            horizontalStackView.distribution = .fillEqually
            (0..<numberOfItemsPerRow).forEach { _ in
                let view = viewsToLayout.isEmpty ? UIView() : viewsToLayout.removeFirst()
                horizontalStackView.addArrangedSubview(view)
            }
            verticalStackView.addArrangedSubview(horizontalStackView)
        }
        return verticalStackView
    }
    

    I can now use this in multiple ways. For instance this will fill the size of a parent:

    let views: [UIView] = (0..<20).map { index in
        let view = [UILabel(), UIButton()].randomElement()!
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = [.red, .blue, .green].randomElement()!
        return view
    }
    let stackedView = StacksView.layoutViewsVertically(views, numberOfItemsPerRow: 3)
    stackedView.frame = view.bounds
    view.addSubview(stackedView)
    

    Or you can have some implicit size defined by views inside the stack view

    let views: [UIView] = (0..<20).map { index in
        let view = [UILabel(), UIButton()].randomElement()!
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = [.red, .blue, .green].randomElement()!
        view.heightAnchor.constraint(equalToConstant: 50).isActive = true
        view.widthAnchor.constraint(equalToConstant: 50).isActive = true
        return view
    }
    let stackedView = StacksView.layoutViewsVertically(views, numberOfItemsPerRow: 3)
    stackedView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(stackedView)
    

    Or you can have something in-between where width is constrained by parent but height is not:

    let views: [UIView] = (0..<20).map { index in
        let view = [UILabel(), UIButton()].randomElement()!
        view.translatesAutoresizingMaskIntoConstraints = false
        view.backgroundColor = [.red, .blue, .green].randomElement()!
        view.heightAnchor.constraint(equalToConstant: 50).isActive = true
        return view
    }
    let stackedView = StacksView.layoutViewsVertically(views, numberOfItemsPerRow: 3)
    stackedView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(stackedView)
    stackedView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 1.0).isActive = true
    
    Login or Signup to reply.
  2. 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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search