You are correct that you need to subclass UICollectionViewLayout.
The essence to understand before starting is that you need to calculate at least position and size for every cell in the collection view. UICollectionViewLayout is just a structured way to provide that information. You get the structure, but you have to provide everything else yourself.
There are 4 methods you need to override:
- prepare
- invalidateLayout
- layoutAttributesForItemAtIndexPath
- layoutAttributesForElementsInRect
One trick is to cache the layout attributes in a lookup table (dictionary):
var cachedItemAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
In prepare, you calculate the layout attributes for each indexPath in your collectionView:
override func prepare() {
super.prepare()
calculateAttributes()
}
In invalidateLayout you reset the cached layout attributes and recalculate them:
override func invalidateLayout() {
super.invalidateLayout()
cachedItemAttributes = [:]
calculateAttributes()
}
In layoutAttributesForItemAtIndexPath you use the lookup table to return the right layout attributes:
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cachedItemAttributes[indexPath]
}
In layoutAttributesForElementsInRect you filter your lookup table for the elements within the specified rect:
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return cachedItemAttributes.filter { rect.intersects($0.value.frame) }.map { $0.value }
}
The final piece of the puzzle is the actual calculation of the layout attributes. Here I will provide only pseudo-code:
func calculateAttributes() {
// For each indexpath (you can get this from the collectionView property using numberOfSections and numberOfItems:inSection )
// calculate the frame, i.e the origin point and size of each cell in your collectionView and set it with UICollectionViewLayoutAttributes.frame
// There are many other properties on UICollectionViewLayoutAttributes that you can tweak for your layout, but frame is a good starting point from which you can start experimenting.
// Add the layout attributes to the lookup table
// end loop
}
To answer your question, here is pseudo-code to calculate the position of each cell:
// If width of cell + current width of row + spacing, insets and margins exceeds the available width
// move to next row.
// else
// cell origin.x = current width of row + interitem spacing
// cell origin.y = number of rows * (row height + spacing)
// endif
If you need your custom layout to be configurable, then either use UICollectionViewDelegateFlowLayout if the available signatures are sufficient, or define your own that inherits from UICollectionViewDelegateFlowLayout or UICollectionViewDelegate. Because your protocol inherits from UICollectionViewDelegateFlowLayout, which itself inherits from UICollectionViewDelegate, you can set it directly as the collectionView delegate in your viewcontroller. In your custom collection view layout you just need to cast the delegate from UICollectionViewDelegate to your custom protocol to use it. Remember to handle cases where the casting fails or where the protocol methods are not implemented by the delegate.