I wanted to create a table like layout (sort of Data tables) but the existing libraries did not provide custom buttons with actions associated to each rows(they only provided textual data in rows) so I created everything from scratch
I subclassed UICollectionViewFlowLayout
to provide my own implementation
import UIKit
class CustomCollectionViewLayout: UICollectionViewFlowLayout {
/// Delegate for passing the information around
weak var delegate: CustomCollectionViewLayoutDelegate?
/// Holds the maximum size required for each column
private var maxSizeForColumn = [Int: CGSize]()
override var collectionViewContentSize: CGSize {
if collectionView?.numberOfSections == 0 { return CGSize.zero }
// Checking if we have all the size available
guard let allCellSize = delegate?.allCellsSize() else { return CGSize.zero }
// Calculating the maximum column size
calculateMaxColumnSize(outOf: allCellSize)
// Calculating the total size required for the layout
let totalSize = maxSizeForColumn.reduce(CGSize.zero) { (partialResult, size) -> CGSize in
CGSize(width: partialResult.width + size.value.width, height: partialResult.height + size.value.height)
}
// Calculating the max height out of all the columns
let maxHeight = maxSizeForColumn.map { $0.value.height }.max() ?? 0
return CGSize(width: totalSize.width, height: CGFloat(allCellSize.count) * maxHeight)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// Calculating the size of the frame
let coordinates = calculateCoordinates(for: indexPath)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(x: coordinates.x, y: coordinates.y, width: maxSizeForColumn[indexPath.row]?.width ?? 0, height: maxSizeForColumn[indexPath.row]?.height ?? 0)
return attributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if collectionView?.numberOfSections == 0 { return nil }
// Fetching all cell size
guard let allCellSize = delegate?.allCellsSize() else { return nil }
// Holds the attributes required for each cell
var attributes : Array<UICollectionViewLayoutAttributes> = [UICollectionViewLayoutAttributes]()
for (sectionIndex, section) in allCellSize.enumerated() {
for (itemIndex,_) in section.enumerated() {
attributes.append(self.layoutAttributesForItem(at: IndexPath(item: itemIndex, section: sectionIndex))!)
}
}
return attributes
}
}
// MARK: - Helper methods
extension CustomCollectionViewLayout {
/// Calculates the coordinates required for laying out each cell
///
/// - Parameters:
///
/// - indexPath: Index path of the cell whose coordinates are to be genereated
///
/// - Returns: Coordinates required for the specified cell
///
private func calculateCoordinates(for indexPath: IndexPath) -> (x: CGFloat, y: CGFloat) {
var x: CGFloat = 0
// Calculating x coordinate
for currentIndex in stride(from: 0, to: indexPath.item, by: 1) {
x += maxSizeForColumn[currentIndex]?.width ?? 0
}
var y: CGFloat = 0
// // Calculating y coordinate
// for currentIndex in stride(from: 0, to: indexPath.section, by: 1) {
// y += maxSizeForColumn[currentIndex]?.height ?? 0
// }
let maxHeight = maxSizeForColumn.map { $0.value.height }.max()!
y = CGFloat(indexPath.section) * maxHeight
return (x,y)
}
/// Calculates the maximum size required for each column
///
/// - Parameter allCellSize: Size of all the cells
///
private func calculateMaxColumnSize(outOf allCellSize: ItemSizeType) {
// Calculating the number of columns
let numberOfColumns = allCellSize[0].count
// Checking size for each column
for currentColumn in stride(from: 0, to: numberOfColumns, by: 1) {
// Holds the current max size
var maxSize = CGSize.zero
for currentRow in stride(from: 0, to: allCellSize.count, by: 1) {
// Fetching the size of the current cell
let currentCellSize = allCellSize[currentRow][currentColumn]
// Checking if maxsize is smaller than the current cell size based on width
maxSize = maxSize.width < currentCellSize.width ? currentCellSize : maxSize
}
maxSizeForColumn[currentColumn] = maxSize
}
}
}
Since the data for the table will be coming from the backend, I don't know the size for each item of the row at compile time, so I use sizeForItemAt
to calculate the size for each item, stores it inside a variable, cellSizes
(of type [[CGSize]]
) and my UICollectionViewFlowLayout
class extract these values using delegates
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// Calculating the size for the item required
let itemSize = calculateItemSize(for: indexPath)
// Adding the cell sizes as per their Index Path
if cellSizes.indices.contains(indexPath.section) {
if cellSizes[indexPath.section].indices.contains(indexPath.row) {
/// Item is already present, so we just update the existing value
cellSizes[indexPath.section][indexPath.row] = itemSize
}
else {
/// Item is not present so we append that item
cellSizes[indexPath.section].append(itemSize)
}
}
else {
/// The whole row is not present so we add the row
cellSizes.append([itemSize])
}
return itemSize
}
My searchBar
's delegate looks like this
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
defer {
entitySearchCollectionView.reloadData()
}
// Fetching the text that has been entered for search
guard let searchedText = searchBar.text, searchedText != "" else {
/// No text entered so we display the complete list
// Assigning the original entities to the filtered list
filteredSearchData = entitySearchData
return
}
/// Text is entered so we filter the list accordingly
filteredSearchData = entitySearchData.filter({ (entity) -> Bool in
(entity.entity?[searchKey]?.value as? String ?? "").localizedCaseInsensitiveContains(searchedText)
})
}
This allows filtering of data and later displaying them according to the searched text. This all works fine for iOS 13.
However, for iOS 12.4(the one I have tried for), when I search a text which leads to reducing the number of rows(from 4 to 3), my app crashes with the following error:
*** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKitCore/UIKit-3698.140/UICollectionViewData.m:447
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView received layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x282aa2c40> {length = 2, path = 2 - 0}'
I have tried a lot of fix but most of them says you need to clear cache in prepare()
method inside UICollectionViewFlowLayout
subclass. But I am now using prepare()
method as when I do this, sizeForItemAt
is not being called and the size calculation for each item is not being performed
Can anyone help me with this. Been stuck on this problem for a while