// // 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 buttonPercentage: CGFloat? 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(for position: ButtonGroup.ButtonPosition, with collectionViewWidth: CGFloat){ var offset = 0.0 let height = rowHeight attributes.last?.spacing = 0 //check to see if you have buttons and there is a percentage if let buttonPercentage, hasButtons, buttonPercentage > 0 { var usedSpace = 0.0 //get the width for the buttons for attribute in attributes { if !attribute.isButton { usedSpace += attribute.frame.width } usedSpace += attribute.spacing } let buttonAvailableSpace = collectionViewWidth - usedSpace let realPercentage = (buttonPercentage / 100) let buttonWidth = realPercentage * buttonAvailableSpace // print("buttonPercentage :\(realPercentage)") // print("collectionView width:\(collectionViewWidth)") // print("usedSpace width:\(usedSpace)") // print("button available width:\(buttonAvailableSpace)") // print("each button width:\(buttonWidth)\n") // print("minimum widht:\(ButtonSize.large.minimumWidth)") // test sizing var testSize = 0.0 var buttonCount = 0.0 for attribute in attributes { if attribute.isButton { testSize += buttonWidth buttonCount += 1 } } if buttonWidth >= ButtonSize.large.minimumWidth { if testSize <= buttonAvailableSpace { for attribute in attributes { if attribute.isButton { attribute.frame.size.width = buttonWidth } } } else { let distributedSize = buttonAvailableSpace / buttonCount for attribute in attributes { if attribute.isButton { attribute.frame.size.width = distributedSize } } } } } 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 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 ButtongGroupPositionLayoutDelegate: AnyObject { func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize func collectionView(_ collectionView: UICollectionView, buttonableAtIndexPath indexPath: IndexPath) -> any Buttonable } class ButtonLayoutAttributes: UICollectionViewLayoutAttributes{ var spacing: CGFloat = 0 var buttonable: Buttonable? var isButton: Bool { guard buttonable is Button else { return false } return true } convenience init(spacing: CGFloat, buttonable: Buttonable, forCellWith indexPath: IndexPath) { self.init(forCellWith: indexPath) self.spacing = spacing self.buttonable = buttonable } } 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: ButtonGroup.ButtonPosition = .left var rowQuantity: Int = 0 var buttonPercentage: CGFloat? 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 // 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 buttonables let totalItems = collectionView.numberOfItems(inSection: section) //create rows var rows = [ButtonCollectionViewRow]() rows.append(ButtonCollectionViewRow()) let collectionViewWidth = collectionView.frame.width for item in 0.. collectionViewWidth || (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(ButtonCollectionViewRow()) } // get the buttonable let itemButtonable = delegate.collectionView(collectionView, buttonableAtIndexPath: indexPath) // see if there is another item in the array let nextItem = item + 1 // if so, get the buttonable // and get the spacing based of the // current buttonable and the next buttonable if nextItem < totalItems { //get the next buttonable let neighbor = delegate.collectionView(collectionView, buttonableAtIndexPath: IndexPath(item: nextItem, section: section)) // get the spacing to go between the current and next buttonable itemSpacing = ButtonGroupConstants.getSpacing(for: .horizontal, with: itemButtonable, neighboring: neighbor) } // create the custom layout attribute let attributes = ButtonLayoutAttributes(spacing: itemSpacing, buttonable: itemButtonable, forCellWith: indexPath) attributes.frame = CGRect(x: 0, y: 0, width: itemSize.width, 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 = ButtonGroupConstants.getVerticalSpacing(for: prevRow, neighboringRow: row) row.rowY = layoutHeight + rowSpacing layoutHeight += rowSpacing } layoutHeight += row.rowHeight } // recalculate rows x based off of positions rows.forEach { $0.buttonPercentage = buttonPercentage $0.layout(for: position, 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 } }