// // RadioSwatchGroup.swift // VDS // // Created by Matt Bruce on 8/25/22. // import Foundation import UIKit import Combine public class RadioSwatchGroup: RadioSwatchGroupBase { public override func didSelect(selector: RadioSwatch) { if let index = model.selectors.firstIndex(where: {$0.selected == true }), let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? CollectionViewCell { cell.modelHandler.toggle() } selector.toggle() label.text = selector.model.text valueChanged() } } public class RadioSwatchGroupBase>: Control, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UICollectionViewDelegate { //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- public var selectedModel: ModelHandlerType.ModelType? { return model.selectedModel } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- public var label = Label() private let cellSize: CGFloat = 48.0 private let labelSpacing: CGFloat = 24.0 private let labelHeight: CGFloat = 16.0 private let lineSpacing: CGFloat = 12.0 private let itemSpacing: CGFloat = 16.0 private var collectionViewHeight: NSLayoutConstraint? private var collectionViewWidth: NSLayoutConstraint? fileprivate lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout().with { $0.minimumLineSpacing = lineSpacing $0.minimumInteritemSpacing = itemSpacing } return UICollectionView(frame: .zero, collectionViewLayout: layout).with { $0.backgroundColor = .clear $0.showsHorizontalScrollIndicator = false $0.showsVerticalScrollIndicator = false $0.isScrollEnabled = false $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(label) addSubview(collectionView) NSLayoutConstraint.activate([ label.topAnchor.constraint(equalTo: topAnchor), label.leadingAnchor.constraint(equalTo: leadingAnchor), label.trailingAnchor.constraint(equalTo: trailingAnchor), label.heightAnchor.constraint(equalToConstant: labelHeight), collectionView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: labelSpacing), collectionView.leadingAnchor.constraint(equalTo: leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) //TODO: Look at this width stuff, we should NOT need it! collectionViewWidth = collectionView.widthAnchor.constraint(equalToConstant: cellSize * 20) collectionViewWidth?.isActive = true collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: cellSize) collectionViewHeight?.isActive = true } open override func layoutSubviews() { super.layoutSubviews() // Accounts for any collection size changes setHeight() DispatchQueue.main.async { self.collectionView.collectionViewLayout.invalidateLayout() } } open func setHeight() { let swatches = model.selectors guard swatches.count > 0 else { collectionViewHeight?.constant = 0 return } // Calculate the height let swatchesInRow = floor(CGFloat(collectionView.bounds.width/(cellSize + itemSpacing))) let numberOfRows = ceil(CGFloat(swatches.count)/swatchesInRow) let height = (numberOfRows * cellSize) + (itemSpacing * (numberOfRows-1)) collectionViewHeight?.constant = CGFloat(height) } public override func initialSetup() { super.initialSetup() collectionView.delegate = self collectionView.dataSource = self } open override func updateView(viewModel: ModelType) { label.set(with: viewModel.labelModel) collectionView.reloadData() setNeedsLayout() } //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.id == viewModel.id }) { model.selectors[index] = viewModel } } //-------------------------------------------------- // MARK: - UICollectionViewDelegateFlowLayout //-------------------------------------------------- open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: cellSize, height: cellSize) } //-------------------------------------------------- // MARK: - 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 } didSelect(selector: cell.modelHandler) } //-------------------------------------------------- // MARK: - UICollectionViewDataSource //-------------------------------------------------- public func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return model.selectors.count } var cellsubs: [Int: AnyCancellable] = [:] public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell", for: indexPath) as? CollectionViewCell else { return UICollectionViewCell() } let model = model.selectors[indexPath.row] cell.modelHandler.isUserInteractionEnabled = false //cancel if sub exists if let sub = cellsubs[indexPath.row] { sub.cancel() cellsubs[indexPath.row] = nil } let sub = cell.modelHandler .handlerPublisher() .sink { [weak self] changed in if cell.modelHandler.shouldUpdateView(viewModel: model) { print("Model Change: \(changed)") self?.replace(viewModel: changed) } } cellsubs[indexPath.row] = sub cell.set(with: model) return cell } open func didSelect(selector: ModelHandlerType) { fatalError("Must override didSelect") } public func valueChanged() { DispatchQueue.main.asyncAfter(deadline: .now() + Constants.ModelStateDebounce) { [weak self] in self?.sendActions(for: .valueChanged) } } public var selectedModelHandler: ModelHandlerType? { guard let index = model.selectors.firstIndex(where: {$0.selected == true }), let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? CollectionViewCell else { return nil } return cell.modelHandler } }