How to Make a Simple Collection View With Swift

How to make a simple collection view with Swift

This project has been tested with Xcode 10 and Swift 4.2.

Create a new project

It can be just a Single View App.

Add the code

Create a new Cocoa Touch Class file (File > New > File... > iOS > Cocoa Touch Class). Name it MyCollectionViewCell. This class will hold the outlets for the views that you add to your cell in the storyboard.

import UIKit
class MyCollectionViewCell: UICollectionViewCell {

@IBOutlet weak var myLabel: UILabel!
}

We will connect this outlet later.

Open ViewController.swift and make sure you have the following content:

import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {

let reuseIdentifier = "cell" // also enter this string as the cell identifier in the storyboard
var items = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "40", "41", "42", "43", "44", "45", "46", "47", "48"]


// MARK: - UICollectionViewDataSource protocol

// tell the collection view how many cells to make
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.items.count
}

// make a cell for each cell index path
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! MyCollectionViewCell

// Use the outlet in our custom class to get a reference to the UILabel in the cell
cell.myLabel.text = self.items[indexPath.row] // The row value is the same as the index of the desired text within the array.
cell.backgroundColor = UIColor.cyan // make cell more visible in our example project

return cell
}

// MARK: - UICollectionViewDelegate protocol

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// handle tap events
print("You selected cell #\(indexPath.item)!")
}
}

Notes

  • UICollectionViewDataSource and UICollectionViewDelegate are the protocols that the collection view follows. You could also add the UICollectionViewFlowLayout protocol to change the size of the views programmatically, but it isn't necessary.
  • We are just putting simple strings in our grid, but you could certainly do images later.

Set up the storyboard

Drag a Collection View to the View Controller in your storyboard. You can add constraints to make it fill the parent view if you like.

Sample Image

Make sure that your defaults in the Attribute Inspector are also

  • Items: 1
  • Layout: Flow

The little box in the top left of the Collection View is a Collection View Cell. We will use it as our prototype cell. Drag a Label into the cell and center it. You can resize the cell borders and add constraints to center the Label if you like.

Sample Image

Write "cell" (without quotes) in the Identifier box of the Attributes Inspector for the Collection View Cell. Note that this is the same value as let reuseIdentifier = "cell" in ViewController.swift.

Sample Image

And in the Identity Inspector for the cell, set the class name to MyCollectionViewCell, our custom class that we made.

Sample Image

Hook up the outlets

  • Hook the Label in the collection cell to myLabel in the MyCollectionViewCell class. (You can Control-drag.)
  • Hook the Collection View delegate and dataSource to the View Controller. (Right click Collection View in the Document Outline. Then click and drag the plus arrow up to the View Controller.)

Sample Image

Finished

Here is what it looks like after adding constraints to center the Label in the cell and pinning the Collection View to the walls of the parent.

Sample Image

Making Improvements

The example above works but it is rather ugly. Here are a few things you can play with:

Background color

In the Interface Builder, go to your Collection View > Attributes Inspector > View > Background.

Cell spacing

Changing the minimum spacing between cells to a smaller value makes it look better. In the Interface Builder, go to your Collection View > Size Inspector > Min Spacing and make the values smaller. "For cells" is the horizontal distance and "For lines" is the vertical distance.

Cell shape

If you want rounded corners, a border, and the like, you can play around with the cell layer. Here is some sample code. You would put it directly after cell.backgroundColor = UIColor.cyan in code above.

cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 1
cell.layer.cornerRadius = 8

See this answer for other things you can do with the layer (shadow, for example).

Changing the color when tapped

It makes for a better user experience when the cells respond visually to taps. One way to achieve this is to change the background color while the cell is being touched. To do that, add the following two methods to your ViewController class:

// change background color when user touches cell
func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = UIColor.red
}

// change background color back when user releases touch
func collectionView(_ collectionView: UICollectionView, didUnhighlightItemAt indexPath: IndexPath) {
let cell = collectionView.cellForItem(at: indexPath)
cell?.backgroundColor = UIColor.cyan
}

Here is the updated look:

Sample Image

Further study

  • A Simple UICollectionView Tutorial
  • UICollectionView Tutorial Part 1: Getting Started
  • UICollectionView Tutorial Part 2: Reusable Views and Cell Selection

UITableView version of this Q&A

  • UITableView example for Swift

How to make square cells with collection view layout in swift

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:

Sample Image Sample Image

Sample Image Sample Image

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.

Selecting cell in 1 Collection View to change second Collection View properties (Xcode)

Declare a var:

class ViewController:
var newColor : UIColor?

Change this:

if cell.isSelected {
cell2.iconimage.backgroundColor = cell.backgroundColor
}

for this:

 if cell.isSelected {
newColor = cell.backgroundColor
iconView.reloadData()
}

in

 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

put this

if newColor != nil { // maybe a better test here, if cell was selected
cell2.iconimage.backgroundColor = newColor
}

before

 return cell2

How to properly manage elaborate Collection View cellForItemAt method?

What you're doing is not "wrong" in and of itself. However, there is a school of thought which says that, ideally, cellForRowAt should know nothing of the internal interface of the cell. This (cellForRowAt) is the data source. It should hand the cell just the data. You have a cell subclass (TaskCell), so it just needs some methods or properties that allow it to be told what the data is, and the cell should then format itself and populate its own interface in accordance with those settings.

If all of that formatting and configuration code is moved into the cell subclass, the cellForRowAt implementation will be much shorter, cleaner, and clearer, and the division of labor will be more appropriate.

In support of this philosophy, I would just add that Apple has adopted it in iOS 14, where a cell can now have a UIContentConfiguration object whose job is to communicate the data from cellForRowAt into the contentView of the cell. So for example instead of saying (for a table view cell) cell.textLabel.text = "howdy" you say configuration.text = "howdy" and let the configuration object worry about the fact that a UILabel might be involved in the interface.



Related Topics



Leave a reply



Submit