updated flow
Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
parent
c3c133cac7
commit
2e519eee28
@ -211,7 +211,7 @@
|
|||||||
EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */; };
|
EAF7F13328A2A16500B287F5 /* AttachmentLabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */; };
|
||||||
EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF978202A99035B00C2FEA9 /* Enabling.swift */; };
|
EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAF978202A99035B00C2FEA9 /* Enabling.swift */; };
|
||||||
EAFD5AA02CB5CA5300C87DE1 /* TileletGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */; };
|
EAFD5AA02CB5CA5300C87DE1 /* TileletGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */; };
|
||||||
EAFD5AA22CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */; };
|
EAFD5AA22CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAFD5AA12CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@ -462,7 +462,7 @@
|
|||||||
EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLabelAttributeModel.swift; sourceTree = "<group>"; };
|
EAF7F13228A2A16500B287F5 /* AttachmentLabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentLabelAttributeModel.swift; sourceTree = "<group>"; };
|
||||||
EAF978202A99035B00C2FEA9 /* Enabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enabling.swift; sourceTree = "<group>"; };
|
EAF978202A99035B00C2FEA9 /* Enabling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Enabling.swift; sourceTree = "<group>"; };
|
||||||
EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroup.swift; sourceTree = "<group>"; };
|
EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroup.swift; sourceTree = "<group>"; };
|
||||||
EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroupPositionLayout.swift; sourceTree = "<group>"; };
|
EAFD5AA12CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileletGroupFlowLayout.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -1180,7 +1180,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */,
|
EAFD5A9F2CB5CA5300C87DE1 /* TileletGroup.swift */,
|
||||||
EAFD5AA12CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift */,
|
EAFD5AA12CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift */,
|
||||||
);
|
);
|
||||||
path = TileletGroup;
|
path = TileletGroup;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -1475,7 +1475,7 @@
|
|||||||
EA336171288B19200071C351 /* VDS.docc in Sources */,
|
EA336171288B19200071C351 /* VDS.docc in Sources */,
|
||||||
EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */,
|
EA985BF02968A93600F2FF2E /* TitleLockupEyebrowModel.swift in Sources */,
|
||||||
EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */,
|
EA5E30532950DDA60082B959 /* TitleLockup.swift in Sources */,
|
||||||
EAFD5AA22CB5CA7900C87DE1 /* TileletGroupPositionLayout.swift in Sources */,
|
EAFD5AA22CB5CA7900C87DE1 /* TileletGroupFlowLayout.swift in Sources */,
|
||||||
EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */,
|
EAD062B02A3B873E0015965D /* BadgeIndicator.swift in Sources */,
|
||||||
183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */,
|
183B16F32C78CF7C00BA6A10 /* CarouselSlotCell.swift in Sources */,
|
||||||
44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */,
|
44A952DD2BE3DA820009F874 /* TableFlowLayout.swift in Sources */,
|
||||||
|
|||||||
@ -10,156 +10,178 @@ import UIKit
|
|||||||
import VDSCoreTokens
|
import VDSCoreTokens
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
open class TileletGroup: View {
|
open class TileletGroup: View {
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Initializers
|
|
||||||
//--------------------------------------------------
|
|
||||||
required public init() {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Public Properties
|
|
||||||
//--------------------------------------------------
|
|
||||||
/// An object containing number of Button components per row for iPhones
|
|
||||||
open var rowQuantityPhone: Int = 0 { didSet { setNeedsUpdate() } }
|
|
||||||
|
|
||||||
/// An object containing number of Button components per row for iPads
|
|
||||||
open var rowQuantityTablet: Int = 0 { didSet { setNeedsUpdate() } }
|
|
||||||
|
|
||||||
/// An object containing number of Button components per row
|
|
||||||
open var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone }
|
|
||||||
|
|
||||||
/// Array of Buttonable Views that are shown in the group.
|
|
||||||
open var tilelets: [Tilelet] = [] { didSet { setNeedsUpdate() } }
|
|
||||||
|
|
||||||
/// Whether this object is enabled or not
|
|
||||||
override open var isEnabled: Bool {
|
|
||||||
didSet {
|
|
||||||
tilelets.forEach { $0.isEnabled = isEnabled }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current Surface and this is used to pass down to child objects that implement Surfacable
|
|
||||||
override open var surface: Surface {
|
|
||||||
didSet {
|
|
||||||
tilelets.forEach { $0.surface = surface }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open var contentSizePublisher: AnyPublisher<CGSize, Never> { collectionView.contentSizePublisher }
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Private Properties
|
// MARK: - Private Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
fileprivate lazy var positionLayout = TileletGroupPositionLayout().with {
|
/// CollectionView to show the rows and columns
|
||||||
|
private lazy var matrixView = SelfSizingCollectionView(frame: .zero, collectionViewLayout: flowLayout).with {
|
||||||
|
$0.register(TileletGroupCellItem.self, forCellWithReuseIdentifier: TileletGroupCellItem.Identifier)
|
||||||
|
$0.dataSource = self
|
||||||
$0.delegate = self
|
$0.delegate = self
|
||||||
|
$0.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
$0.allowsSelection = false
|
||||||
|
$0.showsVerticalScrollIndicator = false
|
||||||
|
$0.showsHorizontalScrollIndicator = false
|
||||||
|
$0.isAccessibilityElement = true
|
||||||
|
$0.backgroundColor = .clear
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CollectionView that renders the array of buttonBase obects.
|
/// Custom flow layout to manage the height of the cells
|
||||||
fileprivate lazy var collectionView: SelfSizingCollectionView = {
|
private lazy var flowLayout = TileletGroupFlowLayout().with {
|
||||||
|
$0.delegate = self
|
||||||
|
$0.scrollDirection = .horizontal
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameter to show the all table rows
|
||||||
|
private var rows: [[Tilelet]] = [] { didSet { setNeedsUpdate() } }
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Enums
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
/// Enums used to define the padding for the cell edge spacing.
|
||||||
|
public enum Padding: String, CaseIterable {
|
||||||
|
case standard, compact
|
||||||
|
|
||||||
return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with {
|
private func value() -> CGFloat {
|
||||||
$0.backgroundColor = .clear
|
switch self {
|
||||||
$0.showsHorizontalScrollIndicator = false
|
case .standard:
|
||||||
$0.showsVerticalScrollIndicator = false
|
return UIDevice.isIPad ? 40.0 : VDSLayout.space3X
|
||||||
$0.isScrollEnabled = false
|
case .compact:
|
||||||
$0.translatesAutoresizingMaskIntoConstraints = false
|
return UIDevice.isIPad ? VDSLayout.space6X : VDSLayout.space3X
|
||||||
$0.dataSource = self
|
}
|
||||||
$0.delegate = self
|
|
||||||
$0.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell")
|
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
func horizontalValue() -> CGFloat { value() }
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Public Methods
|
func verticalValue() -> CGFloat { value() }
|
||||||
//--------------------------------------------------
|
|
||||||
/// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations.
|
|
||||||
open override func setup() {
|
|
||||||
super.setup()
|
|
||||||
addSubview(collectionView)
|
|
||||||
collectionView.pinToSuperView()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Public Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
/// Parameter to set the padding for the cell
|
||||||
|
open var padding: Padding = .standard { didSet { setNeedsUpdate() } }
|
||||||
|
|
||||||
|
/// An object containing number of Button components per row for iPhones
|
||||||
|
open var rowQuantityPhone: Int = 2 { didSet { updateRows() } }
|
||||||
|
|
||||||
|
/// An object containing number of Button components per row for iPads
|
||||||
|
open var rowQuantityTablet: Int = 4 { didSet { updateRows() } }
|
||||||
|
|
||||||
|
/// An object containing number of Button components per row
|
||||||
|
open var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone }
|
||||||
|
|
||||||
|
open var tilelets: [Tilelet] = [] { didSet { updateRows() } }
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Overrides
|
// MARK: - Overrides
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
/// Used to make changes to the View based off a change events or from local properties.
|
|
||||||
|
///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() {
|
open override func updateView() {
|
||||||
super.updateView()
|
super.updateView()
|
||||||
positionLayout.rowQuantity = rowQuantity
|
flowLayout.verticalPadding = padding.verticalValue()
|
||||||
|
flowLayout.horizontalPadding = padding.horizontalValue()
|
||||||
var width: CGFloat?
|
matrixView.reloadData()
|
||||||
tilelets.forEach { tilelet in
|
matrixView.collectionViewLayout.invalidateLayout()
|
||||||
if let width {
|
|
||||||
tilelet.width = width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collectionView.reloadData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func setDefaults() {
|
open override func setDefaults() {
|
||||||
super.setDefaults()
|
super.setDefaults()
|
||||||
rowQuantityPhone = 0
|
padding = .standard
|
||||||
rowQuantityTablet = 0
|
rows = [[]]
|
||||||
tilelets = []
|
|
||||||
}
|
|
||||||
|
|
||||||
open override func reset() {
|
|
||||||
tilelets.forEach { $0.reset() }
|
|
||||||
super.reset()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func layoutSubviews() {
|
func updateRows() {
|
||||||
super.layoutSubviews()
|
var tempRows: [[Tilelet]] = []
|
||||||
// Accounts for any collection size changes
|
for i in stride(from: 0, to: tilelets.count, by: rowQuantity) {
|
||||||
DispatchQueue.main.async { [weak self] in
|
let endIndex = min(i + rowQuantity, tilelets.count)
|
||||||
guard let self else { return }
|
let row = Array(tilelets[i..<endIndex])
|
||||||
self.collectionView.collectionViewLayout.invalidateLayout()
|
tempRows.append(row)
|
||||||
}
|
}
|
||||||
|
rows = tempRows
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TileletGroup: UICollectionViewDataSource, UICollectionViewDelegate {
|
extension TileletGroup: UICollectionViewDelegate, UICollectionViewDataSource, TiletGroupCollectionViewLayoutDataDelegate {
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - UICollectionViewDelegate & UICollectionViewDataSource
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
public func numberOfSections(in collectionView: UICollectionView) -> Int {
|
public func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||||
return 1
|
return rows.count
|
||||||
}
|
}
|
||||||
|
|
||||||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||||
return tilelets.count
|
return rows[section].count
|
||||||
}
|
}
|
||||||
|
|
||||||
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||||
let tilelet = tilelets[indexPath.row]
|
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TileletGroupCellItem.Identifier, for: indexPath) as? TileletGroupCellItem else { return UICollectionViewCell() }
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath)
|
let currentItem = rows[indexPath.section][indexPath.row]
|
||||||
cell.contentView.subviews.forEach { $0.removeFromSuperview() }
|
cell.updateCell(component: currentItem)
|
||||||
cell.contentView.addSubview(tilelet)
|
// let rowColors: (UIColor,UIColor) = indexPath.section % 2 == 0 ? (.green, .yellow) : (.blue, .brown)
|
||||||
tilelet.pinToSuperView()
|
// cell.backgroundColor = indexPath.row % 2 == 0 ? rowColors.0 : rowColors.1
|
||||||
if hasDebugBorder {
|
|
||||||
cell.addDebugBorder()
|
|
||||||
} else {
|
|
||||||
cell.removeDebugBorder()
|
|
||||||
}
|
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
|
//--------------------------------------------------
|
||||||
tilelets[indexPath.row].intrinsicContentSize
|
// MARK: - TableCollectionViewLayoutDataDelegate
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
internal func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> Tilelet {
|
||||||
|
return rows[indexPath.section][indexPath.row]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TileletGroup : TileletGroupPositionLayoutDelegate {
|
final class TileletGroupCellItem: UICollectionViewCell {
|
||||||
func collectionView(_ collectionView: UICollectionView, tileletAtIndexPath indexPath: IndexPath) -> Tilelet {
|
|
||||||
tilelets[indexPath.row]
|
/// Identifier for TableCellItem
|
||||||
|
static let Identifier: String = String(describing: TileletGroupCellItem.self)
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Private Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
/// Main view which holds the content of the cell
|
||||||
|
private let containerView = View()
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Initializers
|
||||||
|
//--------------------------------------------------
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupCell()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setupCell()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupCell() {
|
||||||
|
contentView.backgroundColor = .clear
|
||||||
|
addSubview(containerView)
|
||||||
|
containerView.pinToSuperView()
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Public Methods
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
public func updateCell(component: Tilelet) {
|
||||||
|
containerView.subviews.forEach({ $0.removeFromSuperview() })
|
||||||
|
containerView.addSubview(component)
|
||||||
|
component.pinToSuperView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
142
VDS/Components/Tilelet/TileletGroup/TileletGroupFlowLayout.swift
Normal file
142
VDS/Components/Tilelet/TileletGroup/TileletGroupFlowLayout.swift
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
//
|
||||||
|
// TileletGroupPositionLayout.swift
|
||||||
|
// VDS
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 10/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import VDSCoreTokens
|
||||||
|
|
||||||
|
protocol TiletGroupCollectionViewLayoutDataDelegate: AnyObject {
|
||||||
|
func collectionView(_ collectionView: UICollectionView, dataForItemAt indexPath: IndexPath) -> Tilelet
|
||||||
|
}
|
||||||
|
|
||||||
|
class TileletGroupFlowLayout : UICollectionViewFlowLayout {
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Private Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
/// Parameter to store the layout attributes of cell, while calculating the size & position of the cell
|
||||||
|
private var itemCache: [UICollectionViewLayoutAttributes] = []
|
||||||
|
|
||||||
|
/// Parameter to store the total height of the collectionView
|
||||||
|
private var layoutHeight: CGFloat = 0.0
|
||||||
|
|
||||||
|
/// Parameter to store the total width of the collectionView
|
||||||
|
private var layoutWidth: CGFloat = 0.0
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Public Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
/// Spacing between items in the row
|
||||||
|
public var horizontalPadding: CGFloat = VDSLayout.space1X
|
||||||
|
|
||||||
|
/// Spacing between rows (sections)
|
||||||
|
public var verticalPadding: CGFloat = VDSLayout.space1X
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Internal Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
weak var delegate: TiletGroupCollectionViewLayoutDataDelegate?
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Overrides
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
/// Calculates the layout attribute properties & total height of the collectionView
|
||||||
|
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
|
||||||
|
|
||||||
|
/// Calculate the available width for items (subtract the left and right padding)
|
||||||
|
let collectionViewWidth = collectionView.bounds.width
|
||||||
|
|
||||||
|
/// Determine the number of columns and calculate item width based on available space
|
||||||
|
///
|
||||||
|
let columns = collectionView.numberOfItems(inSection: 0)
|
||||||
|
let totalPadding = horizontalPadding * CGFloat(columns - 1)
|
||||||
|
let availableWidth = collectionViewWidth - totalPadding
|
||||||
|
let itemWidth = availableWidth / CGFloat(columns)
|
||||||
|
|
||||||
|
for currentSection in 0..<sections {
|
||||||
|
let items = collectionView.numberOfItems(inSection: currentSection)
|
||||||
|
|
||||||
|
layoutWidth = 0.0
|
||||||
|
var xPos: CGFloat = 0.0
|
||||||
|
|
||||||
|
/// Looping through all the items in the section, visually these are each column in the row
|
||||||
|
for currentItem in 0..<items {
|
||||||
|
let indexPath = IndexPath(row: currentItem, section: currentSection)
|
||||||
|
let selectedItem = delegate.collectionView(collectionView, dataForItemAt: indexPath)
|
||||||
|
let itemHeight = estimateHeightFor(item: selectedItem, with: itemWidth, index: indexPath)
|
||||||
|
|
||||||
|
layoutWidth += itemWidth + (currentItem > 0 ? horizontalPadding : 0)
|
||||||
|
|
||||||
|
let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
|
||||||
|
attribute.frame = CGRect(x: xPos, y: yPos, width: itemWidth, height: itemHeight)
|
||||||
|
itemCache.append(attribute)
|
||||||
|
|
||||||
|
/// Update x position for the next item
|
||||||
|
xPos += itemWidth + horizontalPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the highest height from all the cells (columns) in the row
|
||||||
|
let highestHeightForSection = itemCache
|
||||||
|
.filter({ $0.indexPath.section == currentSection })
|
||||||
|
.sorted(by: { $0.frame.size.height > $1.frame.size.height })
|
||||||
|
.first?.frame.size.height ?? 0.0
|
||||||
|
|
||||||
|
/// Set the highest height as height to all the cells in the row for uniform height
|
||||||
|
itemCache
|
||||||
|
.filter({ $0.indexPath.section == currentSection })
|
||||||
|
.forEach { attributes in
|
||||||
|
attributes.frame.size.height = highestHeightForSection
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the height to y position for the next section, including vertical padding
|
||||||
|
yPos += highestHeightForSection + verticalPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
layoutHeight = yPos - verticalPadding /// Adjust bottom padding to total height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches estimated height by calling the cell's component estimated height and adding padding
|
||||||
|
private func estimateHeightFor(item: Tilelet, with width: CGFloat, index: IndexPath) -> CGFloat {
|
||||||
|
let itemWidth = width
|
||||||
|
let maxSize = CGSize(width: itemWidth, height: CGFloat.greatestFiniteMagnitude)
|
||||||
|
let estItemSize = item.systemLayoutSizeFitting(maxSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||||
|
return estItemSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This will return the layout attributes for the elements in the defined rect
|
||||||
|
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
|
||||||
|
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
|
||||||
|
for attributes in itemCache {
|
||||||
|
if attributes.frame.intersects(rect) {
|
||||||
|
visibleLayoutAttributes.append(attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return visibleLayoutAttributes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This will return the layout attributes at particular indexPath
|
||||||
|
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
||||||
|
return itemCache.first(where: { $0.indexPath == indexPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the collectionView content size
|
||||||
|
override var collectionViewContentSize: CGSize {
|
||||||
|
return CGSize(width: collectionView?.bounds.width ?? layoutWidth, height: layoutHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,248 +0,0 @@
|
|||||||
//
|
|
||||||
// TileletGroupPositionLayout.swift
|
|
||||||
// VDS
|
|
||||||
//
|
|
||||||
// Created by Matt Bruce on 10/8/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class TileletCollectionViewRow {
|
|
||||||
var attributes = [TileletLayoutAttributes]()
|
|
||||||
|
|
||||||
init() {}
|
|
||||||
|
|
||||||
func add(attribute: TileletLayoutAttributes) {
|
|
||||||
attributes.append(attribute)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rowWidth: CGFloat {
|
|
||||||
return attributes.reduce(0, { result, attribute -> CGFloat in
|
|
||||||
return result + attribute.frame.width + attribute.spacing
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var rowHeight: CGFloat {
|
|
||||||
attributes.compactMap{$0.frame.height}.max() ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxHeightIndexPath: IndexPath {
|
|
||||||
let maxHeight = rowHeight
|
|
||||||
return attributes.first(where: {$0.frame.height == maxHeight})!.indexPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var rowY: CGFloat = 0 {
|
|
||||||
didSet {
|
|
||||||
for attribute in attributes {
|
|
||||||
attribute.frame.origin.y = rowY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func layout(with collectionViewWidth: CGFloat){
|
|
||||||
var offset = 0.0
|
|
||||||
let height = rowHeight
|
|
||||||
|
|
||||||
attributes.last?.spacing = 0
|
|
||||||
|
|
||||||
for attribute in attributes {
|
|
||||||
attribute.frame.origin.x = offset
|
|
||||||
if attribute.frame.height < height {
|
|
||||||
//recalibrate the y to vertically center align rect
|
|
||||||
attribute.frame.origin.y += (height - attribute.frame.size.height) / 2
|
|
||||||
}
|
|
||||||
offset += attribute.frame.width + attribute.spacing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol TileletGroupPositionLayoutDelegate: AnyObject {
|
|
||||||
func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize
|
|
||||||
func collectionView(_ collectionView: UICollectionView, tileletAtIndexPath indexPath: IndexPath) -> Tilelet
|
|
||||||
}
|
|
||||||
|
|
||||||
class TileletLayoutAttributes: UICollectionViewLayoutAttributes{
|
|
||||||
var spacing: CGFloat = 0
|
|
||||||
|
|
||||||
var tilelet: Tilelet?
|
|
||||||
|
|
||||||
convenience init(spacing: CGFloat,
|
|
||||||
tilelet: Tilelet,
|
|
||||||
forCellWith indexPath: IndexPath) {
|
|
||||||
self.init(forCellWith: indexPath)
|
|
||||||
self.spacing = spacing
|
|
||||||
self.tilelet = tilelet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TileletGroupPositionLayout: UICollectionViewLayout {
|
|
||||||
|
|
||||||
weak var delegate: TileletGroupPositionLayoutDelegate?
|
|
||||||
var verticalSpacer: ((TileletCollectionViewRow, TileletCollectionViewRow?) -> CGFloat)?
|
|
||||||
var axisSpacer: ((NSLayoutConstraint.Axis, Tilelet, Tilelet) -> CGFloat)?
|
|
||||||
|
|
||||||
// Total height of the content. Will be used to configure the scrollview content
|
|
||||||
var layoutHeight: CGFloat = 0.0
|
|
||||||
var rowQuantity: Int = 0
|
|
||||||
var tileletPercentage: CGFloat?
|
|
||||||
|
|
||||||
private var itemCache: [TileletLayoutAttributes] = []
|
|
||||||
|
|
||||||
override func prepare() {
|
|
||||||
super.prepare()
|
|
||||||
|
|
||||||
itemCache.removeAll()
|
|
||||||
layoutHeight = 0.0
|
|
||||||
|
|
||||||
guard let collectionView, let delegate else { return }
|
|
||||||
|
|
||||||
// Variable to track the width of the layout at the current state when the item is being drawn
|
|
||||||
var layoutWidthIterator: CGFloat = 0.0
|
|
||||||
|
|
||||||
// Only 1 section in the TileletGroup
|
|
||||||
let section = 0
|
|
||||||
|
|
||||||
// Variables to track individual item width and cumultative height of all items as they are being laid out.
|
|
||||||
var itemSize: CGSize = .zero
|
|
||||||
|
|
||||||
// get number of tilelets
|
|
||||||
let totalItems = collectionView.numberOfItems(inSection: section)
|
|
||||||
|
|
||||||
//create rows
|
|
||||||
var rows = [TileletCollectionViewRow]()
|
|
||||||
rows.append(TileletCollectionViewRow())
|
|
||||||
|
|
||||||
let collectionViewWidth = collectionView.horizontalPinnedWidth() ?? collectionView.frame.width
|
|
||||||
|
|
||||||
for item in 0..<totalItems {
|
|
||||||
|
|
||||||
// start out with no spacing after the item
|
|
||||||
var itemSpacing = 0.0
|
|
||||||
|
|
||||||
// create the indexPath
|
|
||||||
let indexPath = IndexPath(item: item, section: section)
|
|
||||||
|
|
||||||
// get the rect size of the tilelet
|
|
||||||
itemSize = delegate.collectionView(collectionView, sizeForItemAtIndexPath: indexPath)
|
|
||||||
|
|
||||||
// ensure the width is not greater than the collectionViewWidth
|
|
||||||
itemSize.width = min(itemSize.width, collectionViewWidth)
|
|
||||||
|
|
||||||
// determine if the current tilelet will fit in the row
|
|
||||||
let rowItemCount = rows.last?.attributes.count ?? 0
|
|
||||||
|
|
||||||
if (layoutWidthIterator + itemSize.width) > collectionViewWidth && rowQuantity == 0
|
|
||||||
|| (rowQuantity > 0 && rowItemCount == rowQuantity) {
|
|
||||||
|
|
||||||
// If the current row width (after this item being laid out) is exceeding
|
|
||||||
// the width of the collection view content, put it in the next line
|
|
||||||
layoutWidthIterator = 0.0
|
|
||||||
|
|
||||||
// set the spacing of the last item of the current row to 0
|
|
||||||
rows.last?.attributes.last?.spacing = 0
|
|
||||||
|
|
||||||
// add a new row
|
|
||||||
rows.append(TileletCollectionViewRow())
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the tilelet
|
|
||||||
let itemTilelet = delegate.collectionView(collectionView, tileletAtIndexPath: indexPath)
|
|
||||||
|
|
||||||
// see if there is another item in the array
|
|
||||||
let nextItem = item + 1
|
|
||||||
|
|
||||||
// if so, get the tilelet
|
|
||||||
// and get the spacing based of the
|
|
||||||
// current tilelet and the next tilelet
|
|
||||||
if nextItem < totalItems {
|
|
||||||
|
|
||||||
//get the next tilelet
|
|
||||||
let neighbor = delegate.collectionView(collectionView, tileletAtIndexPath: IndexPath(item: nextItem, section: section))
|
|
||||||
|
|
||||||
// get the spacing to go between the current and next tilelet
|
|
||||||
itemSpacing = getAxisSpacing(for: .horizontal, with: itemTilelet, neighboring: neighbor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create the custom layout attribute
|
|
||||||
let attributes = TileletLayoutAttributes(spacing: itemSpacing, tilelet: itemTilelet, forCellWith: indexPath)
|
|
||||||
attributes.frame = CGRect(x: 0, y: 0, width: min(itemSize.width, collectionViewWidth), height: itemSize.height)
|
|
||||||
|
|
||||||
// add it to the array
|
|
||||||
rows.last?.add(attribute: attributes)
|
|
||||||
|
|
||||||
// update the current width
|
|
||||||
// add the current frame width + the found spacing
|
|
||||||
layoutWidthIterator = layoutWidthIterator + attributes.frame.width + itemSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutWidthIterator = 0.0
|
|
||||||
|
|
||||||
// calculate the
|
|
||||||
layoutHeight = 0.0
|
|
||||||
|
|
||||||
// loop through the rows and set
|
|
||||||
// the row y position for each element
|
|
||||||
// also add to the layoutHeight
|
|
||||||
for item in 0..<rows.count {
|
|
||||||
let row = rows[item]
|
|
||||||
var rowSpacing = 0.0
|
|
||||||
|
|
||||||
if item > 0 {
|
|
||||||
let prevRow = rows[item - 1]
|
|
||||||
rowSpacing = getVerticalSpacing(for: prevRow, neighboringRow: row)
|
|
||||||
row.rowY = layoutHeight + rowSpacing
|
|
||||||
layoutHeight += rowSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
layoutHeight += row.rowHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// recalculate rows x based off of positions
|
|
||||||
rows.forEach {
|
|
||||||
$0.layout(with: collectionViewWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
let rowAttributes = rows.flatMap { $0.attributes }
|
|
||||||
itemCache = rowAttributes
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutAttributesForElements(in rect: CGRect)-> [UICollectionViewLayoutAttributes]? {
|
|
||||||
var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
|
|
||||||
|
|
||||||
for attributes in itemCache {
|
|
||||||
if attributes.frame.intersects(rect) {
|
|
||||||
visibleLayoutAttributes.append(attributes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleLayoutAttributes
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
|
|
||||||
return itemCache[indexPath.row]
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getAxisSpacing(for axis: NSLayoutConstraint.Axis, with primary: Tilelet, neighboring: Tilelet) -> CGFloat {
|
|
||||||
20
|
|
||||||
}
|
|
||||||
|
|
||||||
private func getVerticalSpacing(for row: TileletCollectionViewRow, neighboringRow: TileletCollectionViewRow?) -> CGFloat {
|
|
||||||
20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user