Create a grid table with fixed number of rows and columns of variable widths

I've tried a few approaches to create a standard table - with a fixed number of columns and rows, but with variable column widths - for all device classes. The contraint system, even using Stack Views, created an impossible number of layout errors, and so it was suggested to use UICollectionViews, which appears better, but I cannot solve the following issues.


Firstly, the cells in the last row of the table do not align perfectly with the cells above. Each cell is fractionally less width, making the final cell quite clearly unaligned. This issue, while minor in the example project, can appear larger depending on cell contents or device sizes.


Secondly, while I can manage device orientation changes on most devices tested in the simulator, this not true for iPad Air devices. While the table layout is correct when I first run the project on iPad Air in any orientation, and is correct when re-orientated from landscape to horizontal; the layout breaks if I run the project on a iPad Air in portrait orientation, but then rotate to landscape.


To demonstrate these issues, I've put the code below and a very simple project on github (link below). Within the project is a reference to the original source for setting column widths on StackExchange. It would be great if someone with a greater level of undestanding could provide a definitive method, for all devices and in all orientations, to create tables with a fixed number of columns that also have variable widths.


TableRowColumn.zip


    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
    {
        var columnPortion = CGFloat()
        let columnNumber = indexPath.item % 5
        switch columnNumber {
        case 0:
            columnPortion = 0.6
        case 1:
            columnPortion = 0.3
        case 2:
            columnPortion = 1
        case 3:
            columnPortion = 1.4
        case 4:
            columnPortion = 1.7
        default:
            columnPortion = 1
        }
        
        // https://stackoverflow.com/questions/54915227/uicollectionview-remove-space-between-cells-with-7-items-per-row
        var cellWidth = CGFloat()
        let availableWidth = collectionView.bounds.size.width
        print("available width", availableWidth)
        
        let minimumWidth = floor(availableWidth / numberOfColumns)
        print("minmum width", minimumWidth)
        
        cellWidth = minimumWidth * columnPortion - 1  // the - 1 is to remove problems with rounding
        print("cell width", cellWidth)
        
        return CGSize(width: cellWidth, height: rowHeight).xx_rounded()
    }

Replies

The sum of all columnPortion is 5, unless some rounding error.


Could you try the following :


let portion0 : Double = 0.6
let portion1 : Double = 0.3
let portion2 : Double = 1.0
let portion3 : Double = 1.4
let portion4 : Double = 5.0 - (portion0 + portion1 + portion2 + portion3)
switch columnNumber {
case 0:
    columnPortion = portion0 // 0.6
case 1:
    columnPortion = portion1 // 0.3
case 2:
    columnPortion = portion2 // 1
case 3:
    columnPortion = portion3 // 1.4
case 4:
    columnPortion = portion4 // 1.7
default:
    columnPortion = 1
}

This solves the issue on the iPad Air devices - awesome and thanks! It also appears to work if the portions are of type CGFloat, which is what the sizeForItem needs. Please tell me if I need to use Double for the initial values of the portions or CGFloat is ok.


As you may realise, the uneven last row issue is still there. In my humble, it seems most likely to also be a rounding issue in the following line:


cellWidth = minimumWidth * columnPortion - 20


In the initial code (in the question and code on github), the "constant" ("20") value was "1" - as I understand, to soak up any rounding issues. In a lot of cases, the closer this value comes to zero, the closer the widths in the final row are to the other rows - BUT, sometimes (some devices, some orientations), if the value is too low, the cells from one row spill onto the next row (instead of 5 cells/columns per row, only 4). If I increase this constant value, to say 20 as above, it prevents this problem from occuring, but adds space between cells in all rows except the last row, which is the second issue in the question. I've tried to calculate this remainder value, but this also fills the available width in all rows (except the last row) by adding space between the cells.

Great it works, hope you will bne able to solve oher issues to have a totally clean app.


Don't forget to close the thread if OK and good continuation.

Have you any idea why the cell widths in the last row, including their spacing, is different to the other rows? This makes my grids look less table like.

Is it in the last row only ?

Is is the last visible row or the very last row ?


can you describe approximatively what are the cells widths in this last row ?

and maybe post the code of tableview delegate functions.

Yes, it is only the last row. I am not sure the difference between the last visible row or the very last row. In this case, the very last row in the intended table grid is visible. In the simple github project, you can just see the problem in the very last row, although it is not as pronounced as it is in some cases - it just looks a little tiny bit to the left of the cell above. This would not be a problem, but in certain situations it can become quite exacerbated (I will try to explain below).


With regard to the widths, I have logged their values and the widths of each cell are identical. Here is the log output for the final two rows showing width values for 5 cells in a single row (all rows have these exact identical values)

cell width 172.0

cell width 132.0

cell width 132.0

cell width 132.0

cell width 132.00000000000003

cell width 172.0

cell width 132.0

cell width 132.0

cell width 132.0

cell width 132.00000000000003


The width values are always equal for cells in the same column, even when the problem is exacerbated. When exacerbated, space is being added between the cells of each row, except in the very last visible row in the grid. Hence, the cells in this row do not exactly sit below the cells above them like a column of cells (particularly the very last cell in the last column).


What appears to be happening is that when calculating cellWidth, I am subtracting a constant value to solve rounding issues (see all caps comment in code). Depending on the available width, column numbers and cell content, this constant needs to be adjusted to be between 0.5 and 20 (I just increase the value until I get the right number of columns on as many devices as possible). The spaces added between cells are this constant value shared across the entire row because when I increase the value of this constant, the spaces increase. This would be ok, except the spaces are not added between cells in the very last row! Here are the collection view delegate methods:


    func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
        cView.collectionViewLayout.invalidateLayout()
        cView.reloadData()
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int
    { return 1 }
    
    func collectionView( _ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
    { return currentContents.count }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
    {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        
        let settings = currentContents[indexPath.item]
        cell.cVLabel.text = settings.text
        
        cell.contentView.layer.cornerRadius = 5
        cell.contentView.layer.borderWidth = 0.5
        
        cell.contentView.layer.borderColor = UIColor.gray.cgColor
        cell.contentView.layer.masksToBounds = true
        
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
    { return UIEdgeInsets(top: 0, left: 0, bottom: 10, right: 0) }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat
    { return 0 }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat
    { return 0.5 }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
    {
        var columnPortion = CGFloat()
        let columnNumber = indexPath.item % 5
        
        switch columnNumber {
        case 0:
            columnPortion = portion0
        case 1:
            columnPortion = portion1
        case 2:
            columnPortion = portion2
        case 3:
            columnPortion = portion3
        case 4:
            columnPortion = portion4
        default:
            columnPortion = 1
        }
        
        // https://stackoverflow.com/questions/54915227/uicollectionview-remove-space-between-cells-with-7-items-per-row
        var cellWidth = CGFloat()

        let minimumWidth = floor(availableWidth / numberOfColumns)
        
        cellWidth = minimumWidth * columnPortion - 20   // HERE IS CONSTANT TO REMOVE ROUNDING ISSUES, BUT IT ADDS SPACE ISSUES IN ALL ROWS EXCEPT FINAL ROW
        print("cell width", cellWidth)
        
        return CGSize(width: cellWidth, height: rowHeight).xx_rounded()
    }
}

extension CGFloat
{
    func xx_rounded(_ rule: FloatingPointRoundingRule = .down, toDecimals decimals: Int = 0) -> CGFloat
    {
        let multiplier = CGFloat(10 ^ decimals)
        return (self * multiplier).rounded(.down) / multiplier
    }
}

extension CGSize
{
    func xx_rounded(rule: FloatingPointRoundingRule = .down, toDecimals: Int = 0) -> CGSize
    { return CGSize(width: width.xx_rounded(rule, toDecimals: toDecimals),
                    height: height.xx_rounded(rule, toDecimals: toDecimals)) }
}

Thanks for bothering with this - everything I've tried fails because the last row is somehow treated differently - it's quite frustrating.