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