// // Table.swift // VDS // // Created by Nadigadda, Sumanth on 24/04/24. // import Foundation import UIKit import VDSCoreTokens ///Table is view composed of rows and columns, which takes any view into each cell and resizes based on the highest cell height. @objc(VDSTable) open class Table: View { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- /// CollectionView to show the rows and columns private lazy var matrixView = SelfSizingCollectionView(frame: .zero, collectionViewLayout: flowLayout).with { $0.register(TableCellItem.self, forCellWithReuseIdentifier: TableCellItem.Identifier) $0.dataSource = self $0.delegate = self $0.translatesAutoresizingMaskIntoConstraints = false $0.allowsSelection = false $0.showsVerticalScrollIndicator = false $0.showsHorizontalScrollIndicator = false $0.backgroundColor = .clear } /// Custom flow layout to manage the height of the cells private lazy var flowLayout = MatrixFlowLayout().with { $0.delegate = self $0.scrollDirection = .horizontal } /// Array of ``TableItemModel`` by combining Header & Row items private var tableData: [TableRowModel] { return tableHeader + tableRows } //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- /// Enums used to define the padding for the cell edge spacing. public enum Padding: String, CaseIterable { case standard, compact func horizontalValue() -> CGFloat { switch self { case .standard, .compact: return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X } } func verticalValue() -> CGFloat { switch self { case .standard: return UIDevice.isIPad ? VDSLayout.space8X : VDSLayout.space6X case .compact: return UIDevice.isIPad ? VDSLayout.space4X : VDSLayout.space3X } } } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Parameter to set striped status for the table open var striped: Bool = false { didSet { setNeedsUpdate() } } /// Parameter to set the padding for the cell open var padding: Padding = .standard { didSet { setNeedsUpdate() } } /// Parameter to show the table header row open var tableHeader: [TableRowModel] = [] { didSet { setNeedsUpdate() } } /// Parameter to show the all table rows open var tableRows: [TableRowModel] = [] { didSet { setNeedsUpdate() } } open var fillContainer: Bool = true { didSet { setNeedsUpdate() } } open var columnWidths: [CGFloat]? { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- ///Called upon initializing the table view open override func setup() { super.setup() addSubview(matrixView) matrixView.pinToSuperView() } /// Will update the table view, when called becasue of any changes in component parameters open override func updateView() { super.updateView() if fillContainer == true || (fillContainer == false && columnWidths == nil) { columnWidths = calculateColumnWidths() } flowLayout.layoutPadding = padding matrixView.reloadData() matrixView.collectionViewLayout.invalidateLayout() } open override func setDefaults() { super.setDefaults() striped = false padding = .standard tableHeader = [] tableRows = [] fillContainer = true columnWidths = nil } func calculateColumnWidths() -> [CGFloat] { guard let noOfColumns = tableData.first?.columnsCount else { return [] } let itemWidth = floor(matrixView.safeAreaLayoutGuide.layoutFrame.width / CGFloat(noOfColumns)) return Array(repeating: itemWidth, count: noOfColumns) } } extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableCollectionViewLayoutDataDelegate { //-------------------------------------------------- // MARK: - UICollectionViewDelegate & UICollectionViewDataSource //-------------------------------------------------- public func numberOfSections(in collectionView: UICollectionView) -> Int { return tableData.count } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return tableData[section].columnsCount } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() } let currentItem = tableData[indexPath.section].columns[indexPath.row] let shouldStrip = striped ? (indexPath.section % 2 != 0) : false let isHeader = tableData[indexPath.section].isHeader var edgePadding = UIEdgeInsets(top: padding.verticalValue(), left: 0, bottom: padding.verticalValue(), right: padding.horizontalValue()) edgePadding.left = (indexPath.row == 0 && !striped) ? VDSLayout.space1X : padding.horizontalValue() cell.updateCell(content: currentItem, surface: surface, striped: shouldStrip, padding: edgePadding, isHeader: isHeader) setAccessibilityForCell(cell: cell, content: currentItem, path: indexPath) return cell } //-------------------------------------------------- // MARK: - TableCollectionViewLayoutDataDelegate //-------------------------------------------------- func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> TableItemModel { return tableData[indexPath.section].columns[indexPath.row] } func collectionView(_ collectionView: UICollectionView, widthForItemAt indexPath: IndexPath) -> CGFloat { return columnWidths?[indexPath.row] ?? 0.0 } //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- /// To set the accessibility label for the each cell based on the criteria. Table name along with total no of column & row information should be passed in the first cell's accessibility label. private func setAccessibilityForCell(cell: TableCellItem, content: TableItemModel, path: IndexPath) { var accLabel = content.component?.accessibilityLabel ?? "Empty" ///Set the type of header label if path.section == 0 { accLabel.append(", Column Header") } else if path.row == 0 { ///As per design team, inspite of column 0 may not look like a header, it should be read as header. accLabel.append(", Row Header") } ///Set the Row/Column number for each cell if path.row == 0 { accLabel.append(", Row \(path.section + 1), Column \(path.row + 1)") } else { accLabel.append(", Column \(path.row + 1)") } ///Set the Row header accessibilityLabel at the end of the non-header cells accessibilityLabel if path.section != 0, path.row != 0, let columnHeaderAccLabel = tableHeader.first?.columns[path.row].component?.accessibilityLabel { accLabel.append(", \(columnHeaderAccLabel)") } cell.accessibilityLabel = accLabel } }