From 78d97fd936a392f46fe4fe473cde9b46799af251 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Tue, 30 Aug 2022 14:36:13 -0500 Subject: [PATCH] kind of working swatch Signed-off-by: Matt Bruce --- VDS/Components/RadioSwatch/RadioSwatch.swift | 14 +- .../RadioSwatch/RadioSwatchGroup.swift | 204 ++++++++++++++++++ .../RadioSwatch/RadioSwatchGroupModel.swift | 16 ++ .../RadioSwatch/RadioSwatchModel.swift | 27 ++- VDS/Protocols/Surfaceable.swift | 2 +- 5 files changed, 247 insertions(+), 16 deletions(-) diff --git a/VDS/Components/RadioSwatch/RadioSwatch.swift b/VDS/Components/RadioSwatch/RadioSwatch.swift index fcb9e2cc..e9bf95c7 100644 --- a/VDS/Components/RadioSwatch/RadioSwatch.swift +++ b/VDS/Components/RadioSwatch/RadioSwatch.swift @@ -31,9 +31,6 @@ open class RadioSwatchBase: Control, Cha }() public var onChange: Blocks.ActionBlock? - - @Proxy(\.model.id) - open var id: UUID //can't bind to @Proxy open override var isSelected: Bool { @@ -160,13 +157,8 @@ open class RadioSwatchBase: Control, Cha /// Follow the SwiftUI View paradigm /// - Parameter viewModel: state open override func shouldUpdateView(viewModel: ModelType) -> Bool { - let update = viewModel.selected != model.selected - || viewModel.text != model.text - || viewModel.primaryColor != model.primaryColor - || viewModel.secondaryColor != model.secondaryColor - || viewModel.surface != model.surface - || viewModel.disabled != model.disabled - return update + let should = viewModel != model + return should } open override func updateView(viewModel: ModelType) { @@ -176,7 +168,7 @@ open class RadioSwatchBase: Control, Cha setAccessibilityHint(enabled) setAccessibilityValue(viewModel.selected) setAccessibilityLabel(viewModel.selected) - isUserInteractionEnabled = !viewModel.disabled + //isUserInteractionEnabled = !viewModel.disabled setNeedsLayout() layoutIfNeeded() } diff --git a/VDS/Components/RadioSwatch/RadioSwatchGroup.swift b/VDS/Components/RadioSwatch/RadioSwatchGroup.swift index 608d9723..b2d70384 100644 --- a/VDS/Components/RadioSwatch/RadioSwatchGroup.swift +++ b/VDS/Components/RadioSwatch/RadioSwatchGroup.swift @@ -6,3 +6,207 @@ // 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() + } +} diff --git a/VDS/Components/RadioSwatch/RadioSwatchGroupModel.swift b/VDS/Components/RadioSwatch/RadioSwatchGroupModel.swift index 18109458..18660d5a 100644 --- a/VDS/Components/RadioSwatch/RadioSwatchGroupModel.swift +++ b/VDS/Components/RadioSwatch/RadioSwatchGroupModel.swift @@ -6,3 +6,19 @@ // import Foundation + +public protocol RadioSwatchGroupModel: SelectorGroupSelectedModelable, Equatable where SelectorModelType: RadioSwatchModel { } + +public struct DefaultRadioSwatchGroupModel: RadioSwatchGroupModel { + public typealias SelectorModelType = DefaultRadioSwatchModel + public var inputId: String? + public var value: AnyHashable? + public var surface: Surface = .light + public var disabled: Bool = false + public var selectors: [SelectorModelType] + public var selectedModel: DefaultRadioSwatchModel? + public init() { selectors = [] } + public init(selectors: [SelectorModelType]){ + self.selectors = selectors + } +} diff --git a/VDS/Components/RadioSwatch/RadioSwatchModel.swift b/VDS/Components/RadioSwatch/RadioSwatchModel.swift index ced4ad48..cb394ddb 100644 --- a/VDS/Components/RadioSwatch/RadioSwatchModel.swift +++ b/VDS/Components/RadioSwatch/RadioSwatchModel.swift @@ -8,24 +8,43 @@ import Foundation import UIKit -public protocol RadioSwatchModel: Modelable, FormFieldable, DataTrackable, Accessable, Selectable, BinaryColorable { +public protocol RadioSwatchModel: Modelable, FormFieldable, DataTrackable, Accessable, Selectable, BinaryColorable, Equatable { var fillImage: String? { get set } var primaryColor: UIColor? { get set } var secondaryColor: UIColor? { get set } var text: String { get set } - var textAttributes: [LabelAttributeModel]? { get set } var strikethrough: Bool { get set } } public struct DefaultRadioSwatchModel: RadioSwatchModel { - public var id: UUID = UUID() + public static func == (lhs: DefaultRadioSwatchModel, rhs: DefaultRadioSwatchModel) -> Bool { + return lhs.selected == rhs.selected && + lhs.fillImage == rhs.fillImage && + lhs.primaryColor == rhs.primaryColor && + lhs.secondaryColor == rhs.secondaryColor && + lhs.secondaryColor == rhs.secondaryColor && + lhs.strikethrough == rhs.strikethrough && + lhs.inputId == rhs.inputId && + lhs.value == rhs.value && + lhs.surface == rhs.surface && + lhs.disabled == rhs.disabled && + lhs.dataAnalyticsTrack == rhs.dataAnalyticsTrack && + lhs.dataClickStream == rhs.dataClickStream && + lhs.accessibilityHintEnabled == rhs.accessibilityHintEnabled && + lhs.accessibilityHintDisabled == rhs.accessibilityHintDisabled && + lhs.accessibilityValueEnabled == rhs.accessibilityValueEnabled && + lhs.accessibilityValueDisabled == rhs.accessibilityValueDisabled && + lhs.accessibilityLabelEnabled == rhs.accessibilityLabelEnabled && + lhs.accessibilityLabelDisabled == rhs.accessibilityLabelDisabled + } + + public var selected: Bool = false public var fillImage: String? @OptionalCodableColor public var primaryColor: UIColor? @OptionalCodableColor public var secondaryColor: UIColor? public var text: String = "" - public var textAttributes: [LabelAttributeModel]? public var strikethrough: Bool = false public var inputId: String? diff --git a/VDS/Protocols/Surfaceable.swift b/VDS/Protocols/Surfaceable.swift index c00fd04c..d1eec284 100644 --- a/VDS/Protocols/Surfaceable.swift +++ b/VDS/Protocols/Surfaceable.swift @@ -9,7 +9,7 @@ import Foundation import UIKit import VDSColorTokens -public enum Surface: String, Codable { +public enum Surface: String, Codable, Equatable { case light, dark public var color: UIColor { return self == .dark ? VDSColor.backgroundPrimaryDark : .clear