Precalculating the layout

In order to calculate the layout for the collection view, it's nice to use a playground first. A playground enables you to quickly experiment with values, calculations, or other ideas and you don't have to wait for your entire app to build and run every time. You can make quick changes to your code and you'll see the effects of the change almost instantly. You can create a playground by navigating to File | New | Playground.

The approach we'll take to calculating this layout is to assign a row and column to each cell. You don't have to create an actual property that assigns these but you do need a way to map each cell to a position in the grid.

There should be as many contacts on the screen as possible so step 1 is to figure out how many cells fit on the vertical axis of the collection view. The horizontal axis isn't relevant because the layout will scroll horizontally. Imagine the collection view has a height of 667 points. Every cell is 90 points high and they have 10 points of spacing in between them. This means that (667+10) / 100 = 6.77 cells fit on the vertical axis. You need to add 10 points to the height of the collection view because a margin was added to every cell earlier. In reality, the number of cells that have a margin is one less than the total number of cells, so we need to compensate for the 10 extra points that are involved in the calculation. The number of cells per row can now be calculated as well. If there are 60 cells that need to be divided on 6 rows, the number of cells per row is 60 cells / 6 rows = 10 cells per row.

Now that this information is available, a loop can be written that calculates the frame for every cell. Open the playground you created earlier and add the following code to it:

import UIKit 

let collectionViewHeight = 667
let itemHeight = 90
let itemWidth = 100
let itemSpacing = 10

let numberOfItems = 60

let numRows = (collectionViewHeight + itemSpacing) / (itemHeight + itemSpacing)
let numColumns = numberOfItems / numRows

The preceding snippet sets up some variables based on the rules that were established earlier. An important thing to note is that numRows isn't 6.77 but just 6. The reason for this is that we're not going to display partial cells. Using the variables defined, it's now possible to write a loop that calculates the frame for every cell. To do this, you can create a range from 0 to the total amount of items and iterate over it to calculate the frame based on the row and the column the item would be in, as follows:

var allFrames = [CGRect]() 

for itemIndex in 0..<numberOfItems {
let row = itemIndex % numRows
let column = itemIndex / numRows

var xPos = column * (itemWidth + itemSpacing)
if row % 2 == 1 {
xPos += itemWidth / 2
}

var yPos = row * (itemHeight + itemSpacing)

allFrames.append(CGRect(x: xPos, y: yPos, width: itemWidth, height: itemHeight))
}

The preceding code creates an array. This array is then populated with the frame for every item. Note that the modulus operator(%) is used to determine whether an item is in an even row or not. If it isn't, the x position is offset by half the itemWidth. We must do this because in the design, each odd row is offset from the left a little bit more than its siblings in even rows. If you print the result of this loop, you will see the following output:

[(0.0, 0.0, 100.0, 90.0), (50.0, 100.0, 100.0, 90.0), (0.0, 200.0, 100.0, 90.0), (50.0, 300.0, 100.0, 90.0), (0.0, 400.0, 100.0, 90.0), (50.0, 500.0, 100.0, 90.0), (0.0, 600.0, 100.0, 90.0), (50.0, 700.0, 100.0, 90.0), (0.0, 800.0, 100.0, 90.0), (50.0, 900.0, 100.0, 90.0), (110.0, 0.0, 100.0, 90.0), (160.0, 100.0, 100.0, 90.0), (110.0, 200.0, 100.0, 90.0), (160.0, 300.0, 100.0, 90.0), (110.0, 400.0, 100.0, 90.0), (160.0, 500.0, 100.0, 90.0), (110.0, 600.0, 100.0, 90.0), (160.0, 700.0, 100.0, 90.0), (110.0, 800.0, 100.0, 90.0), (160.0, 900.0, 100.0, 90.0), (220.0, 0.0, 100.0, 90.0), (270.0, 100.0, 100.0, 90.0), (220.0, 200.0, 100.0, 90.0), (270.0, 300.0, 100.0, 90.0), (220.0, 400.0, 100.0, 90.0), (270.0, 500.0, 100.0, 90.0), (220.0, 600.0, 100.0, 90.0), (270.0, 700.0, 100.0, 90.0), (220.0, 800.0, 100.0, 90.0), (270.0, 900.0, 100.0, 90.0), (330.0, 0.0, 100.0, 90.0), (380.0, 100.0, 100.0, 90.0), (330.0, 200.0, 100.0, 90.0), (380.0, 300.0, 100.0, 90.0), (330.0, 400.0, 100.0, 90.0), (380.0, 500.0, 100.0, 90.0), (330.0, 600.0, 100.0, 90.0), (380.0, 700.0, 100.0, 90.0), (330.0, 800.0, 100.0, 90.0), (380.0, 900.0, 100.0, 90.0), (440.0, 0.0, 100.0, 90.0), (490.0, 100.0, 100.0, 90.0), (440.0, 200.0, 100.0, 90.0), (490.0, 300.0, 100.0, 90.0), (440.0, 400.0, 100.0, 90.0), (490.0, 500.0, 100.0, 90.0), (440.0, 600.0, 100.0, 90.0), (490.0, 700.0, 100.0, 90.0), (440.0, 800.0, 100.0, 90.0), (490.0, 900.0, 100.0, 90.0), (550.0, 0.0, 100.0, 90.0), (600.0, 100.0, 100.0, 90.0), (550.0, 200.0, 100.0, 90.0), (600.0, 300.0, 100.0, 90.0), (550.0, 400.0, 100.0, 90.0), (600.0, 500.0, 100.0, 90.0), (550.0, 600.0, 100.0, 90.0), (600.0, 700.0, 100.0, 90.0), (550.0, 800.0, 100.0, 90.0), (600.0, 900.0, 100.0, 90.0)] 

This output isn't the easiest to read but if you examine it closely, you'll notice that this result is exactly what it is supposed to be. Every cell has the correct width (100) and height (90), and every other row is indented by 50 points. Now that the formula to calculate these frames is complete, let's create the actual implementation. You've already created a placeholder class for the layout, so open ContactsCollectionViewLayout.swift and add the following skeleton code to it:

import UIKit 

class ContactsCollectionViewLayout: UICollectionViewLayout {
var itemSize = CGSize(width: 110, height: 90)
var itemSpacing: CGFloat = 10

var layoutAttributes = [UICollectionViewLayoutAttributes]()

override var collectionViewContentSize: CGSize {
return CGSize.zero
}

override func prepare() {

}

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect)
-> Bool {
return false
}

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return nil
}

override func layoutAttributesForItem(at indexPath: IndexPath) ->
UICollectionViewLayoutAttributes? {
return nil
}
}

The preceding code implements some placeholders. An important thing to note is that the itemSize and itemSpacing are mutable variables. This allows external sources to update these values if they'd prefer to do so. The default values are the values that make sense for the HelloContacts app so our app won't have to customize these. However, if you reuse this layout, you might want to use different sizes. Also, instead of using an array of CGRect instances, this class uses an array of UICollectionViewLayoutAttributes. This is the type of object that's used to lay out collection view cells.

The implementation for prepare is the following:

private var numberOfItems = 0 
private var numRows = 0
private var numColumns = 0

override func prepare() {
guard let collectionView = collectionView
else { return }
let availableHeight = Int(collectionView.bounds.height + itemSpacing)
let itemHeightForCalculation = Int(itemSize.height + itemSpacing)

numberOfItems = collectionView.numberOfItems(inSection: 0)
numRows = availableHeight / itemHeightForCalculation
numColumns = Int(ceil(CGFloat(numberOfItems) / CGFloat(numRows)))
layoutAttributes.removeAll()

for itemIndex in 0..<numberOfItems {
let row = itemIndex % numRows
let column = itemIndex / numRows

var xPos = column * Int(itemSize.width + itemSpacing)
if row % 2 == 1 {
xPos += Int(itemSize.width / 2)
}

let yPos = row * Int(itemSize.height + itemSpacing)

let index = IndexPath(row: itemIndex, section: 0)
let attributes = UICollectionViewLayoutAttributes(forCellWith: index)
attributes.frame = CGRect(x: CGFloat(xPos), y: CGFloat(yPos), width: itemSize.width, height: itemSize.height)

layoutAttributes.append(attributes)
}
}

This implementation is very similar to the playground implementation. The most important differences are highlighted. The number of items isn't a fixed number anymore but it's determined by asking the collection view for the number of items in section 0. This works because the collection view in HelloContacts only has a single section. If it had more sections, the implementation would have been a bit more complex because it would need to keep track of these sections somehow.

Another difference is the use of UICollectionViewLayoutAttributes. This class contains information about the cell's IndexPath and its frame. The frame is assigned right after it's created and that line is basically the same as it was already in the playground.

This wraps up step 1. The implementation of prepare is complete, and the content size can be derived because of it. Let's move on to the next step and implement the skeleton's collectionViewContentSize computed property.