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,
- consider a cell max width to be 130 and height can grow as per the content.
- the height of a cell should be the same across rows, and the width of a cell should be the same across columns.
- the cell can consist only labels and there is no restriction to show number of lines. So, it can expand based on the content.
- 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
- Initial width not applied in UI but applied in attributes
- 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)")
}
}
CompositionalLayoutis 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?