// // ButtonGroup.swift // VDS // // Created by Matt Bruce on 11/3/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine @objc(VDSButtonGroup) open class ButtonGroup: View, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate, ButtongGroupPositionLayoutDelegate { //-------------------------------------------------- // MARK: - Enums //-------------------------------------------------- public enum ButtonPosition: String, CaseIterable { case left, center, right } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- //An object containing number of Button components per row, in each viewport open var rowQuantityPhone: Int = 0 { didSet { setNeedsUpdate() } } open var rowQuantityTablet: Int = 0 { didSet { setNeedsUpdate() } } open var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone } //If provided, aligns TextLink/TextLinkCaret alignment when rowQuantity is set one. open var buttonPosition: ButtonPosition = .center { didSet { setNeedsUpdate() }} open var buttons: [Buttonable] = [] { didSet { setNeedsUpdate() }} //If provided, width of Button components will be rendered based on this value. If omitted, default button widths are rendered. open var buttonWidth: CGFloat? { didSet { if let buttonWidth, let buttonPercentage, buttonWidth > 0, buttonPercentage > 0{ self.buttonPercentage = nil } buttons.forEach { button in if let button = button as? Button { button.width = buttonWidth } } setNeedsUpdate() } } var _buttonPercentage: CGFloat? open var buttonPercentage: CGFloat? { get { _buttonPercentage } set { if let newValue, newValue <= 100.0, rowQuantity > 0 { _buttonPercentage = newValue } else { _buttonPercentage = nil } if let buttonWidth, let buttonPercentage, buttonWidth > 0, buttonPercentage > 0 { self.buttonWidth = nil } positionLayout.buttonPercentage = buttonPercentage setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- fileprivate lazy var positionLayout = ButtonGroupPositionLayout().with { $0.position = .center $0.delegate = self } fileprivate lazy var collectionView: SelfSizingCollectionView = { return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with { $0.backgroundColor = .clear $0.showsHorizontalScrollIndicator = false $0.showsVerticalScrollIndicator = false $0.isScrollEnabled = false $0.translatesAutoresizingMaskIntoConstraints = false $0.dataSource = self $0.delegate = self $0.register(ButtonGroupCollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell") } }() //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Whether this object is disabled or not override open var disabled: Bool { didSet { buttons.forEach { button in var b = button b.disabled = disabled } } } /// Current Surface and this is used to pass down to child objects that implement Surfacable override open var surface: Surface { didSet { buttons.forEach { button in var b = button b.surface = surface } } } //-------------------------------------------------- // 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 Functions //-------------------------------------------------- open override func setup() { super.setup() addSubview(collectionView) collectionView.pinToSuperView() backgroundColor = .red } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Function used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() positionLayout.position = buttonPosition positionLayout.rowQuantity = rowQuantity collectionView.reloadData() } open override func layoutSubviews() { super.layoutSubviews() // Accounts for any collection size changes DispatchQueue.main.async { [weak self] in guard let self else { return } self.collectionView.collectionViewLayout.invalidateLayout() } } //-------------------------------------------------- // MARK: - UICollectionViewDataSource //-------------------------------------------------- public func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return buttons.count } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let button = buttons[indexPath.row] guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as? ButtonGroupCollectionViewCell else { return UICollectionViewCell() } cell.subviews.forEach { $0.removeFromSuperview() } cell.addSubview(button) button.pinToSuperView() if hasDebugBorder { cell.addDebugBorder() } else { cell.removeDebugBorder() } return cell } public func collectionView(_ collectionView: UICollectionView, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize { buttons[indexPath.row].intrinsicContentSize } public func collectionView(_ collectionView: UICollectionView, isButtonTypeForItemAtIndexPath indexPath: IndexPath) -> Bool { if let _ = buttons[indexPath.row] as? Button { return true } else { return false } } public func collectionView(_ collectionView: UICollectionView, buttonableAtIndexPath indexPath: IndexPath) -> Buttonable { buttons[indexPath.row] } public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { print("index") } }