// // ButtonGroupPositionLayout.swift // VDS // // Created by Matt Bruce on 11/18/22. // import Foundation import UIKit class ButtonCollectionViewRow { var attributes = [ButtonLayoutAttributes]() init() {} func add(attribute: ButtonLayoutAttributes) { attributes.append(attribute) } var hasButtons: Bool { attributes.contains(where: { $0.isButton }) } 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 rowY: CGFloat = 0 { didSet { for attribute in attributes { attribute.frame.origin.y = rowY } } } func layout(for position: ButtonPosition, with collectionViewWidth: CGFloat){ var offset = 0.0 attributes.last?.spacing = 0 switch position { case .left: break case .center: offset = (collectionViewWidth - rowWidth) / 2 case .right: offset = (collectionViewWidth - rowWidth) } for attribute in attributes { attribute.frame.origin.x = offset offset += attribute.frame.width + attribute.spacing } } } public enum ButtonPosition: String, CaseIterable { case left, center, right } protocol ButtongGroupPositionLayoutDelegate: AnyObject { func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize func collectionView(_ collectionView: UICollectionView, buttonableAtIndexPath indexPath: IndexPath) -> any Buttonable func collectionView(_ collectionView: UICollectionView, insetsForItemsInSection section: Int) -> UIEdgeInsets } class ButtonLayoutAttributes: UICollectionViewLayoutAttributes{ var spacing: CGFloat = 0 var isButton: Bool = false convenience init(spacing: CGFloat, forCellWith indexPath: IndexPath) { self.init(forCellWith: indexPath) self.spacing = spacing } } class ButtonGroupPositionLayout: UICollectionViewLayout { weak var delegate: ButtongGroupPositionLayoutDelegate? // Total height of the content. Will be used to configure the scrollview content var layoutHeight: CGFloat = 0.0 var position: ButtonPosition = .left private var itemCache: [ButtonLayoutAttributes] = [] 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 ButtonGroup let section = 0 // Get the necessary data (if implemented) from the delegates else provide default values let insets: UIEdgeInsets = delegate.collectionView(collectionView, insetsForItemsInSection: section) let rowSpacing: CGFloat = ButtonGroupConstants.rowSpacingButton // Variables to track individual item width and cumultative height of all items as they are being laid out. var itemSize: CGSize = .zero // add top layoutHeight += insets.top let totalItems = collectionView.numberOfItems(inSection: section) for item in 0.. collectionView.frame.width { // 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 layoutHeight += itemSize.height + rowSpacing } let itemButtonable = delegate.collectionView(collectionView, buttonableAtIndexPath: indexPath) let nextItem = item + 1 if nextItem < totalItems { let neighbor = delegate.collectionView(collectionView, buttonableAtIndexPath: IndexPath(item: nextItem, section: section)) itemSpacing = ButtonGroupConstants.getHorizontalSpacing(for: itemButtonable, neighboring: neighbor) } let frame = CGRect(x: layoutWidthIterator + insets.left, y: layoutHeight, width: itemSize.width, height: itemSize.height) //print(frame) let attributes = ButtonLayoutAttributes(spacing: itemSpacing, forCellWith: indexPath) attributes.frame = frame attributes.isButton = isButton(buttonable: itemButtonable) itemCache.append(attributes) layoutWidthIterator = layoutWidthIterator + frame.width + itemSpacing } //add bottom layoutHeight += itemSize.height + insets.bottom layoutWidthIterator = 0.0 //Turn into rows and re-calculate var rows = [ButtonCollectionViewRow]() var currentRowY: CGFloat = -1 for attribute in itemCache { if currentRowY != attribute.frame.midY { currentRowY = attribute.frame.midY rows.append(ButtonCollectionViewRow()) } rows.last?.add(attribute: attribute) } //recalculate rows based off of positions rows.forEach { $0.layout(for: position, with: collectionView.frame.width) } let rowAttributes = rows.flatMap { $0.attributes } layoutHeight = insets.top for item in 0.. 0 && item < rows.count { rowSpacing = row.hasButtons ? ButtonGroupConstants.rowSpacingButton : ButtonGroupConstants.rowSpacingTextLink } if item > 0 { row.rowY = layoutHeight + rowSpacing layoutHeight += rowSpacing } layoutHeight += row.rowHeight } layoutHeight += insets.bottom itemCache = rowAttributes } func isButton(buttonable: Buttonable) -> Bool{ if let _ = buttonable as? Button { return true } else { return false } } 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 } let insets = collectionView.contentInset return collectionView.bounds.width - (insets.left + insets.right) } }