如何在 swift 中使用集合视图布局制作方形单元格

How to make square cells with collection view layout in swift

我正在编写一个带有 121 个按钮 (11X11) 的集合视图的游戏,如何将我的集合视图单元格固定为正方形?我想增加或减少单元格的数量,所以布局必须是动态的

这是代码:

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

}

使用集合视图创建精确的网格可能很麻烦。

而且,正如我在评论中提到的,如果您没有利用 UICollectionView 的 built-in 优势——滚动、通过单元格重用进行内存管理等——集合视图可能不是理想的方法。

在不知道您需要做什么的情况下,按钮可能也不是最好的使用方式...

这是一个在堆栈视图中使用按钮的简单示例:

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 {
            [=10=].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
            })
        })

    }
    
}

输出:

点击任何按钮都会使其动画化(使用 post 中的 rotation/scale 代码),并会在调试控制台中打印点击按钮的“行”和“列”。