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