From ed684acd620b6a51c6b76d879e25bc571b4887db Mon Sep 17 00:00:00 2001 From: Sumanth Nadigadda Date: Mon, 6 May 2024 16:00:35 +0530 Subject: [PATCH] Table component, changing the way collectionViewlayout measures the sizes of the cell & minor improvements --- VDS.xcodeproj/project.pbxproj | 4 + VDS/Components/Table/Table.swift | 30 ++--- .../Table/TableCellImageModel.swift | 28 +++++ VDS/Components/Table/TableCellItem.swift | 24 ++-- .../Table/TableCellLabelModel.swift | 18 +-- VDS/Components/Table/TableCellModel.swift | 4 +- VDS/Components/Table/TableFlowLayout.swift | 117 ++++++++++++++++-- 7 files changed, 166 insertions(+), 59 deletions(-) create mode 100644 VDS/Components/Table/TableCellImageModel.swift diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 0d91c8f8..c9df4b12 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 445BA07829C07B3D0036A7C5 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 445BA07729C07B3D0036A7C5 /* Notification.swift */; }; 44604AD429CE186A00E62B51 /* NotificationButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */; }; 44604AD729CE196600E62B51 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44604AD629CE196600E62B51 /* Line.swift */; }; + 446209482BE8E3AF003EBC19 /* TableCellImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 446209472BE8E3AF003EBC19 /* TableCellImageModel.swift */; }; 44A952D92BE384C40009F874 /* TableCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A952D82BE384C40009F874 /* TableCellModel.swift */; }; 44A952DB2BE3852E0009F874 /* TableCellLabelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A952DA2BE3852E0009F874 /* TableCellLabelModel.swift */; }; 44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44A952DC2BE3DA820009F874 /* TableFlowLayout.swift */; }; @@ -219,6 +220,7 @@ 445BA07729C07B3D0036A7C5 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = ""; }; 44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; + 446209472BE8E3AF003EBC19 /* TableCellImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellImageModel.swift; sourceTree = ""; }; 44A952D82BE384C40009F874 /* TableCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellModel.swift; sourceTree = ""; }; 44A952DA2BE3852E0009F874 /* TableCellLabelModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellLabelModel.swift; sourceTree = ""; }; 44A952DC2BE3DA820009F874 /* TableFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableFlowLayout.swift; sourceTree = ""; }; @@ -445,6 +447,7 @@ 44A952DC2BE3DA820009F874 /* TableFlowLayout.swift */, 44A952D82BE384C40009F874 /* TableCellModel.swift */, 44A952DA2BE3852E0009F874 /* TableCellLabelModel.swift */, + 446209472BE8E3AF003EBC19 /* TableCellImageModel.swift */, ); path = Table; sourceTree = ""; @@ -1242,6 +1245,7 @@ EA0D1C392A6AD4DF00E5C127 /* Typography+SpacingConfig.swift in Sources */, EAB2376629E9952D00AABE9A /* UIApplication.swift in Sources */, EAB5FED429267EB300998C17 /* UIView+NSLayoutConstraint.swift in Sources */, + 446209482BE8E3AF003EBC19 /* TableCellImageModel.swift in Sources */, EAB2376829E9992800AABE9A /* TooltipAlertViewController.swift in Sources */, EA33623E2892EE950071C351 /* UIDevice.swift in Sources */, EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, diff --git a/VDS/Components/Table/Table.swift b/VDS/Components/Table/Table.swift index 267ae6a5..40b3e089 100644 --- a/VDS/Components/Table/Table.swift +++ b/VDS/Components/Table/Table.swift @@ -29,10 +29,8 @@ open class Table: View { $0.backgroundColor = .clear } - private let flowLayout = MatrixFlowLayout().with { - $0.estimatedItemSize = UICollectionViewFlowLayout.automaticSize - $0.minimumLineSpacing = 0 - $0.minimumInteritemSpacing = 0 + private lazy var flowLayout = MatrixFlowLayout().with { + $0.delegate = self } //-------------------------------------------------- @@ -77,7 +75,7 @@ open class Table: View { open var rowBottomLineType: Line.Style = .secondary { didSet { setNeedsUpdate() } } - open var tableData: [[TableCellModel]]? { didSet { setNeedsUpdate() } } + open var tableData: [[TableCellModel]] = [] { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Overrides @@ -91,36 +89,32 @@ open class Table: View { open override func updateView() { super.updateView() + flowLayout.layoutPadding = padding matrixView.reloadData() + matrixView.collectionViewLayout.invalidateLayout() } } -extension Table : UICollectionViewDelegate, UICollectionViewDataSource { +extension Table: UICollectionViewDelegate, UICollectionViewDataSource, TableCollectionViewLayoutDataDelegate { public func numberOfSections(in collectionView: UICollectionView) -> Int { - return tableData?.count ?? 0 + return tableData.count } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - return tableData?[section].count ?? 0 + return tableData[section].count } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem, - let currentItem = tableData?[indexPath.section][indexPath.row] - else { return UICollectionViewCell() } + guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TableCellItem.Identifier, for: indexPath) as? TableCellItem else { return UICollectionViewCell() } + let currentItem = tableData[indexPath.section][indexPath.row] let shouldStrip = striped ? (indexPath.section % 2 != 0) : false let style = indexPath.section == 0 ? headerBottomLineType : rowBottomLineType let hideSeparator = indexPath.section == 0 ? headerBottomLine : rowBottomLine cell.updateCell(content: currentItem, surface: surface, separatorStyle: style, isHeader: indexPath.section == 0, hideSeparator: hideSeparator, striped: shouldStrip, padding: padding) return cell } -} - -extension Table: UICollectionViewDelegateFlowLayout { - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - guard let sectionCount = tableData?[indexPath.section].count else { return CGSize.zero } - let width = Int(collectionView.frame.width) / sectionCount - return CGSize(width: width, height: 100) + func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> TableCellModel { + return tableData[indexPath.section][indexPath.row] } } diff --git a/VDS/Components/Table/TableCellImageModel.swift b/VDS/Components/Table/TableCellImageModel.swift new file mode 100644 index 00000000..883315f3 --- /dev/null +++ b/VDS/Components/Table/TableCellImageModel.swift @@ -0,0 +1,28 @@ +// +// TableCellImageModel.swift +// VDS +// +// Created by Nadigadda, Sumanth on 06/05/24. +// + +import Foundation + +extension Table { + + public struct TableCellImageModel: TableCellModel, Surfaceable { + + public var defaultHeight: CGFloat { return 50.0 } + + public var name: Icon.Name + + public var size: Icon.Size + + public var surface: Surface + + public init(name: Icon.Name, size: Icon.Size, surface: Surface = .light) { + self.name = name + self.size = size + self.surface = surface + } + } +} diff --git a/VDS/Components/Table/TableCellItem.swift b/VDS/Components/Table/TableCellItem.swift index 7c2a1cdd..1d1c9255 100644 --- a/VDS/Components/Table/TableCellItem.swift +++ b/VDS/Components/Table/TableCellItem.swift @@ -13,9 +13,7 @@ final class TableCellItem: UICollectionViewCell { static let Identifier: String = String(describing: TableCellItem.self) - private let containerView = View().with { - $0.translatesAutoresizingMaskIntoConstraints = false - } + private let containerView = View() private var cellLabel = Label().with { $0.translatesAutoresizingMaskIntoConstraints = false @@ -34,9 +32,7 @@ final class TableCellItem: UICollectionViewCell { private let backgroundColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundPrimaryLight, VDSColor.backgroundPrimaryDark) private let stripedColorConfiguration = SurfaceColorConfiguration(VDSColor.backgroundSecondaryLight, VDSColor.backgroundSecondaryDark) - private var labelTopConstraint: NSLayoutConstraint? - private var labelBottomConstraint: NSLayoutConstraint? - private var labelTrailingConstraint: NSLayoutConstraint? + private var padding: Table.Padding = .standard override init(frame: CGRect) { super.init(frame: frame) @@ -58,7 +54,7 @@ final class TableCellItem: UICollectionViewCell { func updateCell(content: TableCellModel, surface: Surface, separatorStyle: Line.Style, isHeader: Bool = false, hideSeparator: Bool = false, striped: Bool = false, padding: Table.Padding = .standard) { containerView.subviews.forEach({ $0.removeFromSuperview() }) - + self.padding = padding containerView.surface = surface containerView.backgroundColor = striped ? stripedColorConfiguration.getColor(surface) : backgroundColorConfiguration.getColor(surface) @@ -71,7 +67,7 @@ final class TableCellItem: UICollectionViewCell { containerView.addSubview(separator) separator.pinLeading().pinTrailing().pinBottom() - separator.isHidden = hideSeparator + separator.isHidden = !hideSeparator separator.style = separatorStyle separator.surface = surface } @@ -103,8 +99,12 @@ final class TableCellItem: UICollectionViewCell { return header ? .boldTitleSmall : UIDevice.isIPad ? .bodyLarge : .bodySmall } - override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { - layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(layoutAttributes.frame.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .required) - return layoutAttributes - } +// override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { +// let labelWidth = layoutAttributes.frame.size.width - (2 * padding.horizontalValue()) +// let maxbounds = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude) +// let labelSize = cellLabel.textRect(forBounds: CGRect(origin: .zero, size: maxbounds), limitedToNumberOfLines: cellLabel.numberOfLines) +// var labelHeight = max(labelSize.height, layoutAttributes.frame.size.height) + (2 * padding.horizontalValue()) +// layoutAttributes.frame.size = CGSize(width: layoutAttributes.frame.size.width, height: labelHeight) +// return layoutAttributes +// } } diff --git a/VDS/Components/Table/TableCellLabelModel.swift b/VDS/Components/Table/TableCellLabelModel.swift index 158c9bd4..2f2d4297 100644 --- a/VDS/Components/Table/TableCellLabelModel.swift +++ b/VDS/Components/Table/TableCellLabelModel.swift @@ -6,9 +6,12 @@ // import Foundation + extension Table { public struct TableCellLabelModel: TableCellModel, Surfaceable { + public var defaultHeight: CGFloat { return 50.0 } + public var text: String public var accessibilityString: String? @@ -21,19 +24,4 @@ extension Table { self.surface = surface } } - - public struct TableCellImageModel: TableCellModel, Surfaceable { - - public var name: Icon.Name - - public var size: Icon.Size - - public var surface: Surface - - public init(name: Icon.Name, size: Icon.Size, surface: Surface = .light) { - self.name = name - self.size = size - self.surface = surface - } - } } diff --git a/VDS/Components/Table/TableCellModel.swift b/VDS/Components/Table/TableCellModel.swift index 38ac8a3f..5e92ebe8 100644 --- a/VDS/Components/Table/TableCellModel.swift +++ b/VDS/Components/Table/TableCellModel.swift @@ -7,4 +7,6 @@ import Foundation -public protocol TableCellModel { } +public protocol TableCellModel { + var defaultHeight: CGFloat { get } +} diff --git a/VDS/Components/Table/TableFlowLayout.swift b/VDS/Components/Table/TableFlowLayout.swift index 8cb92de2..8b6f313b 100644 --- a/VDS/Components/Table/TableFlowLayout.swift +++ b/VDS/Components/Table/TableFlowLayout.swift @@ -6,25 +6,116 @@ // import UIKit +import VDSTokens -final class MatrixFlowLayout : UICollectionViewFlowLayout { +protocol TableCollectionViewLayoutDataDelegate: AnyObject { + func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> TableCellModel +} + +class MatrixFlowLayout : UICollectionViewLayout { + + weak var delegate: TableCollectionViewLayoutDataDelegate? + + var layoutPadding: Table.Padding = .standard + + var itemCache: [UICollectionViewLayoutAttributes] = [] + + var layoutHeight: CGFloat = 0.0 + + override func prepare() { + super.prepare() + + itemCache.removeAll() + + layoutHeight = 0.0 + + guard let collectionView, let delegate else { return } + + let sections = collectionView.numberOfSections + + var yPos: CGFloat = 0.0 + + for currentSection in 0.. $1.frame.size.height }).first?.frame.size.height ?? 0.0 + + itemCache.filter({$0.indexPath.section == currentSection}).forEach { attributes in + attributes.frame.size.height = highestHeightForSection + } + + yPos += highestHeightForSection + } + + layoutHeight = yPos + } + + + private func estimateHeightFor(item: TableCellModel, with width: CGFloat, padding: Table.Padding, isHeader: Bool) -> CGFloat { + + if let model = item as? Table.TableCellLabelModel { + return estimatedHeightForLabel(item: model, with: width, padding: padding, isHeader: isHeader) + } + return (item.defaultHeight + (2 * padding.verticalValue())) + } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { - guard let layoutAttributesObjects = super.layoutAttributesForElements(in: rect) else { return nil } - layoutAttributesObjects.forEach({ layoutAttributes in - if layoutAttributes.representedElementCategory == .cell, - let newFrame = layoutAttributesForItem(at: layoutAttributes.indexPath)?.frame { - layoutAttributes.frame = newFrame + var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = [] + for attributes in itemCache { + if attributes.frame.intersects(rect) { + visibleLayoutAttributes.append(attributes) } - }) - return layoutAttributesObjects + } + return visibleLayoutAttributes } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { - guard let collectionView = collectionView, - let layoutAttributes = super.layoutAttributesForItem(at: indexPath) else { return nil } - let itemsCount = CGFloat(collectionView.numberOfItems(inSection: indexPath.section)) - layoutAttributes.frame.size.width = ceil(collectionView.safeAreaLayoutGuide.layoutFrame.width / itemsCount) - return layoutAttributes + return itemCache.filter({ $0.indexPath == indexPath}).first + } + + override var collectionViewContentSize: CGSize { + return CGSize(width: contentWidth, height: layoutHeight) + } + + private var contentWidth: CGFloat { + guard let collectionView = collectionView else { return 0 } + return collectionView.bounds.width + } +} + +extension MatrixFlowLayout { + + private func estimatedHeightForLabel(item: Table.TableCellLabelModel, with width: CGFloat, padding: Table.Padding, isHeader: Bool) -> CGFloat { + + let cellLabel = Label() + cellLabel.textStyle = isHeader ? .boldTitleSmall : UIDevice.isIPad ? .bodyLarge : .bodySmall + cellLabel.text = item.text + + let labelWidth = width - padding.horizontalValue() - VDSLayout.space1X + let maxbounds = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude) + let labelSize = cellLabel.sizeThatFits(maxbounds) + return labelSize.height + (2 * padding.verticalValue()) } }