vds_ios/VDS/Components/Buttons/ButtonGroup/ButtonGroupPositionLayout.swift
2023-09-14 11:47:24 -05:00

303 lines
10 KiB
Swift

//
// 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.Alignment, 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 >= Button.Size.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, buttonBaseAtIndexPath indexPath: IndexPath) -> ButtonBase
}
class ButtonLayoutAttributes: UICollectionViewLayoutAttributes{
var spacing: CGFloat = 0
var button: ButtonBase?
var isButton: Bool {
guard button is Button else { return false }
return true
}
convenience init(spacing: CGFloat,
button: ButtonBase,
forCellWith indexPath: IndexPath) {
self.init(forCellWith: indexPath)
self.spacing = spacing
self.button = button
}
}
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.Alignment = .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 buttons
let totalItems = collectionView.numberOfItems(inSection: section)
//create rows
var rows = [ButtonCollectionViewRow]()
rows.append(ButtonCollectionViewRow())
let collectionViewWidth = 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 button
itemSize = delegate.collectionView(collectionView, sizeForItemAtIndexPath: indexPath)
// determine if the current button will fit in the row
let rowItemCount = rows.last?.attributes.count ?? 0
if (layoutWidthIterator + itemSize.width) > 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 button
let itemButtonBase = delegate.collectionView(collectionView, buttonBaseAtIndexPath: indexPath)
// see if there is another item in the array
let nextItem = item + 1
// if so, get the button
// and get the spacing based of the
// current button and the next button
if nextItem < totalItems {
//get the next button
let neighbor = delegate.collectionView(collectionView, buttonBaseAtIndexPath: IndexPath(item: nextItem, section: section))
// get the spacing to go between the current and next button
itemSpacing = ButtonGroupConstants.getSpacing(for: .horizontal, with: itemButtonBase, neighboring: neighbor)
}
// create the custom layout attribute
let attributes = ButtonLayoutAttributes(spacing: itemSpacing, button: itemButtonBase, 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..<rows.count {
let row = rows[item]
var rowSpacing = 0.0
if item > 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
}
}