// // 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 { didChange() } } open var rowQuantityTablet: Int = 0 { didSet { didChange() } } public var rowQuantity: Int { UIDevice.isIPad ? rowQuantityTablet : rowQuantityPhone } //If provided, aligns TextLink/TextLinkCaret alignment when rowQuantity is set one. open var buttonPosition: ButtonPosition = .center { didSet { didChange() }} open var buttons: [Buttonable] = [] { didSet { didChange() }} //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 } } didChange() } } 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 didChange() } } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private let lineSpacing: CGFloat = 12.0 private let itemSpacing: CGFloat = 16.0 private let estimatedCellHeight: CGFloat = 40.0 private let estimatedCellWidth: CGFloat = 150.0 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 //-------------------------------------------------- override public var disabled: Bool { didSet { buttons.forEach { button in button.disabled = disabled } } } override public var surface: Surface { didSet { buttons.forEach { button in button.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() isAccessibilityElement = true accessibilityTraits = .button addSubview(collectionView) collectionView.pinToSuperView() } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- 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 { 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) cell.buttonable = button button.pinLeading() button.pinTrailing() button.centerYAnchor.constraint(equalTo: cell.centerYAnchor).isActive = true 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] } }