Pre-calculating the layout

To figure out how to calculate the layout for your collection view, you can use a playground to quickly test and iterate over your attempts at calculating a layout, without the overhead of having to run an app first. Playgrounds are a fantastic way to test, explore, and validate ideas you have quickly. Since playgrounds don't need an entire app to run, they provide immediate feedback, something you don't get when you usually build and run a project. Create a new playground by navigating to File | New | Playground. Create a new playground and name it something like LayoutExploration.

The collection view should display as many items as possible, so the first step when calculating the layout is to determine how many cells fit on the vertical axis of the screen. The horizontal axis doesn't need to be taken into account since the horizontal axis is the scrolling axis. Imagine a collection view that has a height of 667 points, where every cell is 90 points high and there are 10 points of spacing between every cell. Using these values, you would perform the following calculation to come up with the number of cells that fit on the vertical axis of the screen:

(667 + 10) / 100 = 6.77

This means that you would fit 6.77 cells on the vertical axis of the collection view. Note that 10 is added to the height of the collection view and that the resulting value is divided by 100. This is done to properly compensate for the spacing between cells. Once you know the number of cells that fit on the vertical axis, you can also determine how many cells will exist on the horizontal axis. All you need to know is the total number of contacts that the collection view is going to display. When the collection view will show 60 cells in total, you can use the following calculation to determine the number of cells per row:

60 / 6 = 10

Note that the number of cells is not divided by 6.77 but by 6. The number of cells on the vertical axis is rounded down because cells should always fit on the screen completely, not partially. The vertical axis only fits six complete cells, so dividing by 6 is the correct way to perform the calculation.

With this information in mind, you can begin setting up the playground by defining a couple of variables. 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 numberOfRows = (collectionViewHeight + itemSpacing) / (itemHeight + itemSpacing)
let numberOfColumns = numberOfItems / numberOfRows

This snippet sets up all the variables that you have seen earlier. Note that numberOfRows does not need to be rounded down. All the input numbers have the type Int. Since Int doesn't support floating point numbers, all calculations done with Int are automatically rounded to the correct value. You can now use these values to write a loop that calculates the exact location for each of the 60 cells that you want to display. The following code snippet does just that:

let allFrames: [CGRect] = (0..<numberOfItems).map { itemIndex in
  let row = itemIndex % numberOfRows
  let column = itemIndex / numberOfRows

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

  let yPosition = row * (itemHeight + itemSpacing)

  return CGRect(x: xPosition, y: yPosition, width: itemWidth, height: itemHeight)
}

print(allFrames)

The preceding code creates an array of CGRect objects by mapping over a range. Whenever you have a list of something - in this case, a list of indexes that is created using (0..<numberOfItems) - you can then use map to transform each item in that list into something else. In this case, map turns an itemIndex object that corresponds to one of the cells that the collection will display into a CGRect object that could be used as the frame object for a cell. This loop also takes care of that playful offset every other row should have. The % operator is used to determine whether the item belongs on an even or odd row and then offsets the item accordingly.

After the array of frames is created, it is printed to make sure the layout contains all the expected values. You can now run your playground by hitting Shift + Enter or clicking the play button in the bottom-left corner of the editor. After running your playground, you should see the following output in the console:

[(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), (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), (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), (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), (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), (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), (660.0, 0.0, 100.0, 90.0), (710.0, 100.0, 100.0, 90.0), (660.0, 200.0, 100.0, 90.0), (710.0, 300.0, 100.0, 90.0), (660.0, 400.0, 100.0, 90.0), (710.0, 500.0, 100.0, 90.0), (770.0, 0.0, 100.0, 90.0), (820.0, 100.0, 100.0, 90.0), (770.0, 200.0, 100.0, 90.0), (820.0, 300.0, 100.0, 90.0), (770.0, 400.0, 100.0, 90.0), (820.0, 500.0, 100.0, 90.0), (880.0, 0.0, 100.0, 90.0), (930.0, 100.0, 100.0, 90.0), (880.0, 200.0, 100.0, 90.0), (930.0, 300.0, 100.0, 90.0), (880.0, 400.0, 100.0, 90.0), (930.0, 500.0, 100.0, 90.0), (990.0, 0.0, 100.0, 90.0), (1040.0, 100.0, 100.0, 90.0), (990.0, 200.0, 100.0, 90.0), (1040.0, 300.0, 100.0, 90.0), (990.0, 400.0, 100.0, 90.0), (1040.0, 500.0, 100.0, 90.0)]

If you want to run your complete playground again, click the stop button in the bottom-left corner to reset the playground and then hit play again to run it. The output you produced is not very easy to read but if you examine it closely, you should be able to see that this layout is exactly what you were looking for. All items have the correct size, the indentation works, and the items are created from top to bottom and from left to right. You have already created a placeholder file called ContactsCollectionViewLayout; now add the following skeleton code to it as a first step toward implementing your custom layout:

import UIKit

class ContactsCollectionViewLayout: UICollectionViewLayout {
  private let itemSize = CGSize(width: 100, height: 90)
  private let itemSpacing: CGFloat = 10

  private var layoutAttributes = [UICollectionViewLayoutAttributes]()

  override var collectionViewContentSize: CGSize {
    return .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
  }
}

All this code does is provide some placeholders that you will gradually replace with proper implementations. Note that itemSize and itemSpacing are defined as constants here. If you were to create your own layout and you want it to behave a bit more like UICollectionViewFlowLayout by allowing developers to customize the item sizes and spacing, you should make these constants variables instead.

In this case, the item size and spacing won't be modified externally so having them as constants makes sense. Because external sources don't need to modify or even access any of the defined properties, they are marked as private. It is considered good practice to mark properties and methods as private if other classes do not need to access them.

Also note that the layout is not defined as an array of CGRect instances, but instead it is defined as an array of UICollectionViewLayoutAttributes. This is the type of object that a collection view uses to describe the layout attributes of its items. You can now add the following implementation for prepare():

override func prepare() {
  // 1
  guard let collectionView = self.collectionView
    else { return }

  let availableHeight = Int(collectionView.bounds.height + itemSpacing)
  let itemHeight = Int(itemSize.height + itemSpacing)

  numberOfItems = collectionView.numberOfItems(inSection: 0)
  numberOfRows = availableHeight / itemHeight
  numberOfColumns = Int(ceil(CGFloat(numberOfItems) / CGFloat(numberOfRows)))

  layoutAttributes.removeAll()

  // 2
  layoutAttributes = (0..<numberOfItems).map { itemIndex in
    let row = itemIndex % numberOfRows
    let column = itemIndex / numberOfRows

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

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

    // 3
    let indexPath = IndexPath(row: itemIndex, section: 0)
    let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
    attributes.frame = CGRect(x: CGFloat(xPosition), y: CGFloat(yPosition),
 width: itemSize.width, height: itemSize.height)

    return attributes
  }
}

This implementation is very similar to the one you wrote in the playground. The first comment highlights a key difference though. Instead of using hardcoded values for the available space, the layout's collectionView property is used to determine the available space.

The second comment shows the code that calculates the position of every item. Just like before, map is used to configure the list of attributes. Note that many conversions between Int and CGFloat take place to make sure all types are used correctly. Since iOS expresses positions in a layout using CGFloat but the layout calculations rely on the rounding properties of Int, it is important to convert between types properly.

The third and last comment highlights the creation of the UICollectionViewLayoutAttributes instance for each item. In addition to a CGRect instance for size and positioning, UICollectionViewLayoutAttributes uses an IndexPath object to tie itself to the position of a certain cell. This wraps up the preparation of the layout. Next up, we will calculate collectionViewContentSize.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset