'How to make square cells with collection view layout in swift

I'm programming a game with a collection view with 121 buttons (11X11), how can I fix my Collection View cells to be a square? I want to increase or decrease the number of cells so the layout has to be dynamic

This is the code:

import UIKit

class GameViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    let reuseIdentifier="cell"
    @IBOutlet var collectionView: UICollectionView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.collectionViewLayout = generateLayout()
    }
    
    @objc
    func animate(for sender:UIButton){
        UIView.animate(withDuration: 0.5, delay: 0, animations: {
            let rotate=CGAffineTransform(rotationAngle: .pi/2)
            let scale=CGAffineTransform(scaleX: 0.5, y: 0.5)
            sender.transform=rotate.concatenating(scale)
        },completion: {_ in
            UIView.animate(withDuration: 0.5, animations: {
                sender.transform=CGAffineTransform.identity
            })
        })
    }
    
    func generateLayout()->UICollectionViewCompositionalLayout{
        let padding:CGFloat=2
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .fractionalHeight(1)
        )
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        
        item.contentInsets=NSDirectionalEdgeInsets(
            top: 0, leading: padding, bottom: 0, trailing: padding
        )
        
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(1),
            heightDimension: .absolute(40)
        )
        
        let group = NSCollectionLayoutGroup.horizontal(
            layoutSize: groupSize,
            subitem: item,
            count: 11
        )
        group.interItemSpacing = .fixed(padding)
        group.contentInsets=NSDirectionalEdgeInsets(top: 0, leading: padding, bottom: 0, trailing: padding)
        
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing=padding
        section.contentInsets=NSDirectionalEdgeInsets(
            top: padding,
            leading: 0,
            bottom: padding,
            trailing: 0
        )
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        
        return layout
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        // #warning Incomplete implementation, return the number of sections
        return 11
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        // #warning Incomplete implementation, return the number of items
        return 11
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! ButtonCollectionViewCell
        cell.layoutGridCells(at:indexPath)
        cell.delegate=self
    
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        let size = collectionView.bounds.size.height

        return CGSize(width: size, height: size)
    }

}


Solution 1:[1]

It can be cumbersome to create exact grids with a collection view.

And, as I mentioned in my comments, if you're not utilizing the built-in advantages of a UICollectionView -- scrolling, memory management via cell reuse, etc -- a collection view may not be the ideal approach.

Without knowing exactly what you need to do, buttons may not be the best to use either...

Here's a quick example using buttons in stack views:

class ButtonGridVC: UIViewController {
    
    // vertical axis stack view to hold the "row" stack views
    let outerStack: UIStackView = {
        let v = UIStackView()
        v.axis = .vertical
        v.distribution = .fillEqually
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    let promptLabel = UILabel()

    // spacing between buttons
    let gridSpacing: CGFloat = 2.0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // let's add a prompt label and a stepper
        //  for changing the grid size
        let stepperStack = UIStackView()
        stepperStack.spacing = 8
        stepperStack.translatesAutoresizingMaskIntoConstraints = false
        
        let stepper = UIStepper()
        stepper.minimumValue = 2
        stepper.maximumValue = 20
        stepper.addTarget(self, action: #selector(stepperChanged(_:)), for: .valueChanged)
        stepper.setContentCompressionResistancePriority(.required, for: .vertical)
        
        stepperStack.addArrangedSubview(promptLabel)
        stepperStack.addArrangedSubview(stepper)
        
        view.addSubview(stepperStack)
        view.addSubview(outerStack)
        
        let g = view.safeAreaLayoutGuide

        // these constraints at less-than-required priority
        //  will make teh outer stack view as large as will fit
        let cw = outerStack.widthAnchor.constraint(equalTo: g.widthAnchor)
        cw.priority = .required - 1
        let ch = outerStack.heightAnchor.constraint(equalTo: g.heightAnchor)
        ch.priority = .required - 1

        NSLayoutConstraint.activate([
            
            // prompt label and stepper at the top
            stepperStack.topAnchor.constraint(greaterThanOrEqualTo: g.topAnchor, constant: 8.0),
            stepperStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            
            // constrain outerStack
            //  square (1:1 ratio)
            outerStack.widthAnchor.constraint(equalTo: outerStack.heightAnchor),

            // don't make it larger than availble space
            outerStack.topAnchor.constraint(greaterThanOrEqualTo: stepperStack.bottomAnchor, constant: gridSpacing),
            outerStack.leadingAnchor.constraint(greaterThanOrEqualTo: g.leadingAnchor, constant: gridSpacing),
            outerStack.trailingAnchor.constraint(lessThanOrEqualTo: g.trailingAnchor, constant: -gridSpacing),
            outerStack.bottomAnchor.constraint(lessThanOrEqualTo: g.bottomAnchor, constant: -gridSpacing),

            // center horizontally and vertically
            outerStack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            outerStack.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            // active width/height constraints created above
            cw, ch,
            
        ])

        // spacing between buttons
        outerStack.spacing = gridSpacing
        
        // we'll start with an 11x11 grid
        stepper.value = 11
        makeGrid(11)
    }
    
    @objc func stepperChanged(_ stpr: UIStepper) {
        // stepper changed, so generate new grid
        makeGrid(Int(stpr.value))
    }
    
    func makeGrid(_ n: Int) {
        // grid must be between 2x2 and 20x20
        guard n < 21, n > 1 else {
            print("Invalid grid size: \(n)")
            return
        }
        
        // clear the existing buttons
        outerStack.arrangedSubviews.forEach {
            $0.removeFromSuperview()
        }
        
        // update the prompt label
        promptLabel.text = "Grid Size: \(n)"
        
        // for this example, we'll use a font size of 8 for a 20x20 grid
        //  adjusting it 1-pt larger for each smaller grid size
        let font: UIFont = .systemFont(ofSize: CGFloat(8 + (20 - n)), weight: .light)
        
        // generate grid of buttons
        for _ in 0..<n {
            // create a horizontal "row" stack view
            let rowStack = UIStackView()
            rowStack.spacing = gridSpacing
            rowStack.distribution = .fillEqually
            // add it to the outer stack view
            outerStack.addArrangedSubview(rowStack)
            // create buttons and add them to the row stack view
            for _ in 0..<n {
                let b = UIButton()
                b.backgroundColor = .systemBlue
                b.setTitleColor(.white, for: .normal)
                b.setTitleColor(.lightGray, for: .highlighted)
                b.setTitle("X", for: [])
                b.titleLabel?.font = font
                b.addTarget(self, action: #selector(gotTap(_:)), for: .touchUpInside)
                rowStack.addArrangedSubview(b)
            }
        }
    }
    
    @objc func gotTap(_ btn: UIButton) {
        // if we want a "row, column" reference to the tapped button
        if let rowStack = btn.superview as? UIStackView {
            if let colIdx = rowStack.arrangedSubviews.firstIndex(of: btn),
               let rowIdx = outerStack.arrangedSubviews.firstIndex(of: rowStack)
            {
                print("Tapped on row: \(rowIdx) column: \(colIdx)")
            }
        }
        
        // animate the tapped button
        UIView.animate(withDuration: 0.5, delay: 0, animations: {
            let rotate = CGAffineTransform(rotationAngle: .pi/2)
            let scale = CGAffineTransform(scaleX: 0.5, y: 0.5)
            btn.transform = rotate.concatenating(scale)
        }, completion: {_ in
            UIView.animate(withDuration: 0.5, animations: {
                btn.transform = CGAffineTransform.identity
            })
        })

    }
    
}

The output:

enter image description here enter image description here

enter image description here enter image description here

Tapping on any button will animate it (using the rotation/scale code from your post), and will print the "Row" and "Column" of the tapped button in the debug console.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 DonMag