Table component, changing the way collectionViewlayout measures the sizes of the cell & minor improvements

This commit is contained in:
Sumanth Nadigadda 2024-05-06 16:00:35 +05:30
parent e1589577fe
commit ed684acd62
7 changed files with 166 additions and 59 deletions

View File

@ -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 = "<group>"; };
44604AD329CE186A00E62B51 /* NotificationButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationButtonModel.swift; sourceTree = "<group>"; };
44604AD629CE196600E62B51 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
446209472BE8E3AF003EBC19 /* TableCellImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellImageModel.swift; sourceTree = "<group>"; };
44A952D82BE384C40009F874 /* TableCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellModel.swift; sourceTree = "<group>"; };
44A952DA2BE3852E0009F874 /* TableCellLabelModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableCellLabelModel.swift; sourceTree = "<group>"; };
44A952DC2BE3DA820009F874 /* TableFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableFlowLayout.swift; sourceTree = "<group>"; };
@ -445,6 +447,7 @@
44A952DC2BE3DA820009F874 /* TableFlowLayout.swift */,
44A952D82BE384C40009F874 /* TableCellModel.swift */,
44A952DA2BE3852E0009F874 /* TableCellLabelModel.swift */,
446209472BE8E3AF003EBC19 /* TableCellImageModel.swift */,
);
path = Table;
sourceTree = "<group>";
@ -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 */,

View File

@ -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]
}
}

View File

@ -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
}
}
}

View File

@ -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
// }
}

View File

@ -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
}
}
}

View File

@ -7,4 +7,6 @@
import Foundation
public protocol TableCellModel { }
public protocol TableCellModel {
var defaultHeight: CGFloat { get }
}

View File

@ -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..<sections {
let items = collectionView.numberOfItems(inSection: currentSection)
for currentItem in 0..<items {
let indexPath = IndexPath(row: currentItem, section: currentSection)
let itemWidth = floor(collectionView.safeAreaLayoutGuide.layoutFrame.width / CGFloat(items))
let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath)
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth, padding: layoutPadding, isHeader: currentSection == 0)
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let origin = CGPoint(x: itemWidth * CGFloat(indexPath.row), y: yPos)
let size = CGSize(width: itemWidth, height: itemHeight)
attribute.frame = CGRect(origin: origin, size: size)
itemCache.append(attribute)
}
let highestHeightForSection = itemCache.sorted(by: {$0.frame.size.height > $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())
}
}