0

Now I want to show table with N number of rows and N number of columns with both directional scrolling.

Constraints

By using compositional layout, we need to design a table structured collectionView. Below are the properties,

  1. consider a cell max width to be 130 and height can grow as per the content.
  2. the height of a cell should be the same across rows, and the width of a cell should be the same across columns.
  3. the cell can consist only labels and there is no restriction to show number of lines. So, it can expand based on the content.
  4. there can be n number of tables in a view.

This is the actual constraints But I am tried with example constraints with exact layout

In My code the Output having the same height for the entire group(row) applied. and using this function layoutAttributesForElements I am manually calculated the max height and width for items also X & Y axes. It's applied in attributes. But in the initial view(UI) width isn't not applied for the items in a same column if I scroll horizontally to end of the layout, every items are arranged properly for the expected width and height.After that while scrolling some cells are hidden but items having the expected sizes.

Problems in UI

  1. Initial width not applied in UI but applied in attributes
  2. Items Hidden while scrolling

Custom Layout


class CustomLayout: UICollectionViewCompositionalLayout {
    
    var rows: Int = 0
    var columns: Int = 0
    var cachedMaxWidths: [CGFloat] = []
    var cachedMaxHeights: [CGFloat] = []

    init(rows: Int, columns: Int, sectionProvider: @escaping UICollectionViewCompositionalLayoutSectionProvider) {
        super.init(sectionProvider: sectionProvider)
        self.rows = rows
        self.columns = columns
        cachedMaxWidths = Array(repeating: CGFloat(0), count: columns)
        cachedMaxHeights = Array(repeating: CGFloat(0), count: rows)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func prepare() {
        super.prepare()
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        
        super.layoutAttributesForItem(at: indexPath)
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)
        
        var columnAttr = Array(repeating: [UICollectionViewLayoutAttributes](), count: columns)
        var rowAttr = Array(repeating: [UICollectionViewLayoutAttributes](), count: rows)
        
        attributes?.forEach { attribute in
            guard attribute.zIndex != 1 else { return }
            let column = attribute.indexPath.item % columns
            let row = attribute.indexPath.item / columns
            columnAttr[column].append(attribute)
            rowAttr[row].append(attribute)
        }
        
        var calx: CGFloat = 0
        for (index, singleColumnAttr) in columnAttr.enumerated() {
            var maxWidth: CGFloat = 0
            for attr in singleColumnAttr {
                maxWidth = max(maxWidth, attr.frame.size.width)
            }
            cachedMaxWidths[index] = max(maxWidth, cachedMaxWidths[index])
            for attr in singleColumnAttr {
                //setting width
                attr.frame.size.width =  maxWidth
                //setting X axis
                attr.frame.origin.x = calx
            }
            calx += maxWidth
        }
        
        var caly: CGFloat = 0
        for (index, singleRowAttr) in rowAttr.enumerated() {
            var maxHeight: CGFloat = 0
            for attr in singleRowAttr {
                maxHeight = max(maxHeight, attr.frame.size.height)
            }
            cachedMaxHeights[index] = max(maxHeight, cachedMaxHeights[index])
            for attr in singleRowAttr {
                // Setting height
                attr.frame.size.height = maxHeight
                // Setting Y axis
                attr.frame.origin.y = caly
            }
            caly += maxHeight
        }
        return attributes
    }
    
    override func invalidateLayout() {
        super.invalidateLayout()
    }
}

MY Controller View Code is


class ViewController: UIViewController, UICollectionViewDataSource {
    
    var modelTableData: ModelTabledata! = nil
    var modelData : [TableData]! = nil
    var flattenedData : [FlattenData]! = nil
    
    private var collectionView : UICollectionView! = nil
    
    private func createLayout() -> UICollectionView {
        let layout = CustomLayout(rows: (self.modelData[0].data.rows.count + 1), columns: (self.modelData[0].data.headers.count)) { (sectionIndex, _) -> NSCollectionLayoutSection in
            let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .estimated(10),heightDimension: .uniformAcrossSiblings(estimate: 10)))
                
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .estimated(10), heightDimension: .estimated(10)), subitems: Array(repeating: item, count: (self.modelData[sectionIndex].data.headers.count)))
                
                // Define the container group where groups will be laid out vertically
            let containerGroup = NSCollectionLayoutGroup.vertical(layoutSize: .init(widthDimension: .estimated(10), heightDimension: .estimated(10)), subitems: Array(repeating: group, count: (self.modelData[sectionIndex].data.rows.count + 1)))
                
                // Create a section with the container group
                let section = NSCollectionLayoutSection(group: containerGroup)
                
                // Define the size of the section header
                let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
                let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top)
//                section.boundarySupplementaryItems = [sectionHeader]
                
                // Set orthogonal scrolling behavior for the section
                section.orthogonalScrollingBehavior = .continuous
            
            return section
        }
        
        return UICollectionView(frame: .zero, collectionViewLayout: layout)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        modelTableData = ModelTabledata()
        modelData = modelTableData.jsonContents
        flattenedData = modelTableData.flattenData()
        self.view.backgroundColor = .blue
        collectionView = createLayout()
        collectionView.dataSource = self
        collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: CollectionViewCell.cellId)
        collectionView.register(TableViewTitle.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: TableViewTitle.sectionHeaderId)
        setupCollectionView()
    }
    
    func setupCollectionView() {
        self.view.addSubview(collectionView)

        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return flattenedData.count
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return flattenedData[section].data.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.cellId, for: indexPath) as! CollectionViewCell
        
        let sectionData = flattenedData[indexPath.section]
        let rowData = sectionData.data[indexPath.item]
            
        cell.configure(with: rowData)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
       // Configure section header
        let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: TableViewTitle.sectionHeaderId, for: indexPath) as! TableViewTitle
       headerView.titleLabel.text = "Section \(indexPath.section)"
       return headerView
   }
    
}

TableData format


struct TableData: Hashable, Codable {
    var type: String
    var data: Table
    var title: String
}

struct Table: Codable, Hashable {
    var rows: [[String]]
    var headers: [String]
}

Loading Json Data


struct FlattenData {
    var type: String
    var data: [String]
    var title: String
}

@Observable
class ModelTabledata {
    var jsonContents: [TableData] = load("data.json")
    
    func flattenData() -> [FlattenData] {
        var customizedData: [FlattenData] = [ ]
        jsonContents.prefix(1).forEach{ table in
            var cellsData : [String] = table.data.rows.flatMap{ $0 }
            cellsData.insert(contentsOf: table.data.headers, at: 0)
            customizedData.append(FlattenData(type: table.type, data: cellsData, title: table.title))
        }
        return customizedData
        
        
    }
}


func load<T: Decodable>(_ filename: String) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
    }

    do {
        data = try Data(contentsOf: file)
//        print(data)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }

    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}
3
  • I don't think CompositionalLayout is going to work for this... How many rows and columns do you anticipate having? You say "cell max width to be 130" -- I assume there is also a min width? Is the data dynamic? That is, will it be updated while the user is scrolling around the grid? Commented Mar 13, 2024 at 11:35
  • Now I am adding some code together but the small problems are there in the layout. So please review Commented Mar 13, 2024 at 12:52
  • In order to try and help, I would need to run your code to see what's happening. Put together an example project (including sample data) and post it somewhere such as GitHub. Commented Mar 13, 2024 at 13:08

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.