3

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

user121095
  • 697
  • 2
  • 6
  • 18

0 Answers0