Understanding the UICollectionViewFlowLayout and its delegate

In its simplest form, a UICollectionView has a grid layout. In this layout, all items are evenly spaced and sized. You can easily see this if you open the storyboard for the HelloContacts app. Select the prototype cell in the collection view, give it a background color, and then run the app. Doing this makes the grid layout very visible; it also shows how the constraints you set up earlier nicely center the cell's contents.

The ease of use and performance of UICollectionView make implementing grid layouts a breeze. However, the current implementation of the grid is not perfect yet. The grid looks alright on an iPhone 6s but on an iPhone SE, the layout looks like it's falling apart and it doesn't look much better when viewed on an iPhone 6s Plus. Let's see if we can fix this by making the layout a bit more dynamic.

In the storyboard, select the Collection View Flow Layout in the Document Outline. In the Attributes Inspector, you can change the scroll direction for a UICollectionView. This is something that a UITableView couldn't do; it only scrolls vertically. If you don't have enough contacts to make the collection view scroll, refer back to Chapter 1, UITableView Touch Up. The workaround presented there can easily be adapted for the collection view.

When you switch to the Size Inspector, you'll see that there are options available that change the cell's spacing and size. You might know that an iPhone SE is 320 points wide, so let's update the item size for the layout object from 110 to 106. This will make a grid that has cells with just a single pixel of spacing on the iPhone SE. Also, update the minimum spacing properties for cells and lines to a value of 1. These values indicate the minimum spacing that should be taken into account for the cells in the layout. In practice, the spacing could be more if this allows the cells to fit better. However, it will never be less than the value specified.

If you build and run now, your layout will look great on the iPhone SE. However, larger phones have different spacing between the cells, whereas the line spacing is always just 1  point. Luckily, we can dynamically manipulate the layout in the code to make sure the grid looks just right on all screen sizes.

The examples above illustrate that UICollectionViewFlowLayout provides a pretty powerful solution for grid layouts, but it's not perfect. Different screen sizes require different cell sizes, and we simply can't set this through Interface Builder. Usually, you'll want your layouts to look perfect on any device, regardless of screen size.

UICollectionViewFlowLayout has a delegate protocol called UICollectionViewDelegateFlowLayout, which allows you to implement a few customization points for the layout. For instance, you can dynamically calculate cell sizes or manipulate the cell spacing. In this case, we'll leave the minimum cell spacing as it is: we want a space between cells that is as small as possible, but not smaller than 1 pixel.

The line spacing should be the same as the cell spacing (1 pixel or more), and the cell size should be dynamic so it covers as much of the horizontal space as possible. The delegate method that you need to implement in order to provide dynamic cell sizes is collectionView(_:layout:sizeForItemAt:). We'll use this to return a value that's approximately a third or less of the width of the UICollectionView and 90 points in height. First, add a new extension to ViewController:

extension ViewController: UICollectionViewDelegateFlowLayout {

}

Then, implement the following UICollectionViewDelegateFlowLayout method in the extension:

func collectionView(_ collectionView: UICollectionView, 
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: floor((collectionView.bounds.width - 2) / 3), height: 90)
}

This method is called for every cell, and it dynamically calculates cell sizes at the time they are needed. All that this method does is figure out how wide a cell should be, based on the width of the collection view it belongs to. From the collection view width, 2 is subtracted because that's the minimum amount of spacing between items so that space can't be used in this calculation.

If you run the app now, you'll notice that the spacing between cells is always nice and tight. However, the spacing between lines is slightly off. You may have to look real close to notice it but it will be off by a little bit when compared to the spacing between cells. This can be fixed by implementing the collectionView(_:layout:minimumLineSpacingForSectionAt:) method. This method is called to figure out the spacing between rows. By using a minimum width of 1, combined with a calculation similar to how the cell width was determined, you can figure out what the correct line spacing should be as follows:

func collectionView(_ collectionView: UICollectionView, 
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
let cellsPerRow: CGFloat = 3
let widthRemainder = (collectionView.bounds.width -
(cellsPerRow-1)).truncatingRemainder(dividingBy: cellsPerRow)
/ (cellsPerRow-1)
return 1 + widthRemainder
}

First, a variable with the number of cells per row is set. This is 3 for the current layout. Then, the same calculation is done as before when calculating cell size; this time, the result will be the remainder of this calculation. The remainder is then divided by the number of gutters that will be on screen. The calculation works like this because the remainder is the number of pixels that will be distributed between the gutters. In order to get the spacing between each gutter, you need to divide by the number of gutters. Finally, the return value is 1 + widthRemainder. The minimum spacing we always want is 1 and widthRemainder is the extra spacing that will be added to these gutters. If you check the spacing now, it will be exactly equal between both the cells and the lines.

The combination of what's been provided out of the box and what's been gained by implementing just a couple of delegate methods is extremely powerful. We can create beautiful, tight grids with just a few calculations. However, sometimes you will want more than just a grid. Maybe you're looking for something that looks a bit more playful? A layout where cells are laid out as if they have been scattered across the screen evenly? This is all possible by implementing a custom UICollectionViewLayout.