// // RadioSwatchGroup.swift // VDS // // Created by Matt Bruce on 8/25/22. // import Foundation import UIKit public class RadioSwatchGroup: Control, Changable { public typealias ModelHandlerType = RadioSwatch //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @Proxy(\.model.selectedModel) public var selectedModel: ModelHandlerType.ModelType? public var onChange: Blocks.ActionBlock? //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var mainStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.spacing = 24 } }() private var label = Label() private let cellSize: CGFloat = 48.0 private let lineSpacing: CGFloat = 12.0 private let itemSpacing: CGFloat = 16.0 public var collectionViewHeight: NSLayoutConstraint? public var collectionViewWidth: NSLayoutConstraint? private lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout().with { $0.minimumLineSpacing = lineSpacing $0.minimumInteritemSpacing = itemSpacing $0.estimatedItemSize = UICollectionViewFlowLayout.automaticSize } return UICollectionView(frame: .zero, collectionViewLayout: layout).with { $0.backgroundColor = .clear $0.translatesAutoresizingMaskIntoConstraints = false $0.register(CollectionViewCell.self, forCellWithReuseIdentifier: "collectionViewCell") } }() //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- override public var disabled: Bool { didSet { updateSelectors() } } override public var surface: Surface { didSet { updateSelectors() } } open override func setup() { super.setup() isAccessibilityElement = true accessibilityTraits = .button addSubview(mainStackView) mainStackView.addArrangedSubview(label) mainStackView.addArrangedSubview(collectionView) NSLayoutConstraint.activate([ mainStackView.topAnchor.constraint(equalTo: topAnchor), mainStackView.leadingAnchor.constraint(equalTo: leadingAnchor), mainStackView.trailingAnchor.constraint(equalTo: trailingAnchor), mainStackView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) collectionViewHeight = collectionView.heightAnchor.constraint(greaterThanOrEqualToConstant: cellSize) collectionViewHeight?.isActive = true collectionViewWidth = collectionView.widthAnchor.constraint(greaterThanOrEqualToConstant: cellSize * 5) collectionViewWidth?.isActive = true } open override func shouldUpdateView(viewModel: ModelType) -> Bool { return viewModel != model } public override func initialSetup() { super.initialSetup() collectionView.delegate = self collectionView.dataSource = self } open override func updateView(viewModel: ModelType) { collectionView.reloadData() setNeedsLayout() } //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func layoutSubviews() { super.layoutSubviews() // Accounts for any collection size changes setHeight() DispatchQueue.main.async { [weak self] in self?.collectionView.collectionViewLayout.invalidateLayout() } } open func setHeight() { let bounds = collectionView.bounds if bounds.width <= 0 { return } if model.selectors.count > 0 { // Calculate the height let swatchesInRow = floor(CGFloat(bounds.width/(cellSize + itemSpacing))) let numberOfRows = ceil(CGFloat(model.selectors.count)/swatchesInRow) let height = (numberOfRows * cellSize) + (itemSpacing * (numberOfRows-1)) if let oldHeight = collectionViewHeight?.constant, height != oldHeight { } collectionViewHeight?.constant = CGFloat(height) } else { collectionViewHeight?.constant = 0 } } //Refactor into new CollectionView Selector protocol public func updateSelectors(){ let selectors = model.selectors.compactMap { existing in return existing.copyWith { $0.disabled = disabled $0.surface = surface } } model.selectors = selectors } public func replace(viewModel: ModelHandlerType.ModelType){ if let index = model.selectors.firstIndex(where: { element in return element.inputId == viewModel.inputId }) { model.selectors[index] = viewModel } } } extension RadioSwatchGroup: UICollectionViewDelegateFlowLayout { open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: cellSize, height: cellSize) } } extension RadioSwatchGroup: UICollectionViewDelegate { open func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { return !model.selectors[indexPath.row].disabled } open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let cell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell else { return } //reset the old model if let selectedModel { let oldSelectedModel = selectedModel.copyWith { $0.selected = false } replace(viewModel: oldSelectedModel) } //set the new model let newSelectedModel = cell.model.copyWith { $0.selected = true } label.text = newSelectedModel.text replace(viewModel: newSelectedModel) selectedModel = newSelectedModel } open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { guard let cell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell else { return } cell.isSelected = false } } extension RadioSwatchGroup: UICollectionViewDataSource { public func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return model.selectors.count } public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as? CollectionViewCell let model = model.selectors[indexPath.row] cell?.modelHandler.isUserInteractionEnabled = false cell?.set(with: model) return cell ?? UICollectionViewCell() } }