diff --git a/MVMCoreUI/Atomic/Atoms/Views/RadioBox.swift b/MVMCoreUI/Atomic/Atoms/Views/RadioBox.swift index 616c7fd6..da762dbb 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/RadioBox.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/RadioBox.swift @@ -20,10 +20,59 @@ open class RadioBox: Control { private var strikeLayer: CALayer? private var maskLayer: CALayer? + public var subTextLabelHeightConstraint: NSLayoutConstraint? + public var radioBoxModel: RadioBoxModel? { return model as? RadioBoxModel } + // MARK: - MVMCoreViewProtocol + + open override func updateView(_ size: CGFloat) { + super.updateView(size) + label.updateView(size) + subTextLabel.updateView(size) + layer.setNeedsDisplay() + } + + open override func setupView() { + super.setupView() + + layer.delegate = self + layer.borderColor = UIColor.black.cgColor + layer.borderWidth = 1 + + label.numberOfLines = 1 + addSubview(label) + NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: innerPadding, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding) + + subTextLabel.textColor = .mvmCoolGray6 + subTextLabel.numberOfLines = 1 + addSubview(subTextLabel) + NSLayoutConstraint.constraintPinSubview(subTextLabel, pinTop: false, topConstant:0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding) + bottomAnchor.constraint(greaterThanOrEqualTo: subTextLabel.bottomAnchor, constant: innerPadding).isActive = true + subTextLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 2).isActive = true + subTextLabelHeightConstraint = subTextLabel.heightAnchor.constraint(equalToConstant: 0) + subTextLabelHeightConstraint?.isActive = true + + addTarget(self, action: #selector(selectBox), for: .touchUpInside) + } + + // MARK: - MoleculeViewProtocol + + public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + guard let model = model as? RadioBoxModel else { return } + isSelected = model.selected + isEnabled = model.enabled + label.text = model.text + subTextLabel.text = model.subText + isOutOfStock = model.strikethrough + subTextLabelHeightConstraint?.isActive = (subTextLabel.text?.count ?? 0) == 0 + } + + // MARK: - State Handling + open override func draw(_ layer: CALayer, in ctx: CGContext) { // Draw the strikethrough strikeLayer?.removeFromSuperlayer() @@ -52,46 +101,21 @@ open class RadioBox: Control { } } - open override func updateView(_ size: CGFloat) { - super.updateView(size) - label.updateView(size) - subTextLabel.updateView(size) + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any size changes layer.setNeedsDisplay() } - open override func setupView() { - super.setupView() - - layer.delegate = self - layer.borderColor = UIColor.black.cgColor - layer.borderWidth = 1 - - label.numberOfLines = 1 - addSubview(label) - NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: innerPadding, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding) - - subTextLabel.textColor = .mvmCoolGray6 - subTextLabel.numberOfLines = 1 - addSubview(subTextLabel) - NSLayoutConstraint.constraintPinSubview(subTextLabel, pinTop: false, topConstant:0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding) - bottomAnchor.constraint(lessThanOrEqualTo: subTextLabel.bottomAnchor, constant: innerPadding).isActive = true - subTextLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 2).isActive = true - - addTarget(self, action: #selector(touched), for: .touchUpInside) + @objc open func selectBox() { + isSelected = true + radioBoxModel?.selected = isSelected + layer.setNeedsDisplay() } - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - super.set(with: model, delegateObject, additionalData) - guard let model = model as? RadioBoxModel else { return } - isSelected = model.selected - isEnabled = model.enabled - label.text = model.text - subTextLabel.text = model.subText - isOutOfStock = model.strikethrough - } - - @objc open func touched() { - isSelected = !isSelected + @objc open func deselectBox() { + isSelected = false + radioBoxModel?.selected = isSelected layer.setNeedsDisplay() } diff --git a/MVMCoreUI/Atomic/Atoms/Views/RadioBoxModel.swift b/MVMCoreUI/Atomic/Atoms/Views/RadioBoxModel.swift index 89bdc842..caf26800 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/RadioBoxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/RadioBoxModel.swift @@ -7,7 +7,7 @@ // import Foundation -@objcMembers public class RadioBoxModel: MoleculeModelProtocol { +@objcMembers public class RadioBoxModel: MoleculeModelProtocol, FormFieldProtocol { public static var identifier: String = "radioBox" public var text: String public var subText: String? @@ -18,8 +18,9 @@ import Foundation public var strikethrough: Bool = false public var fieldValue: String? public var fieldKey: String? - public var groupName: String? - + public var groupName: String = FormValidator.defaultGroupName + public var baseValue: AnyHashable? + private enum CodingKeys: String, CodingKey { case moleculeName case text @@ -34,6 +35,10 @@ import Foundation case groupName } + public func formFieldValue() -> AnyHashable? { + return selected + } + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) text = try typeContainer.decode(String.self, forKey: .text) @@ -53,9 +58,13 @@ import Foundation if let isStrikeTrough = try typeContainer.decodeIfPresent(Bool.self, forKey: .strikethrough) { strikethrough = isStrikeTrough } + fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue) fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) - groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) + if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + self.groupName = groupName + } + baseValue = selected } public func encode(to encoder: Encoder) throws { @@ -70,6 +79,6 @@ import Foundation try container.encode(strikethrough, forKey: .strikethrough) try container.encodeIfPresent(fieldValue, forKey: .fieldValue) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) - try container.encodeIfPresent(groupName, forKey: .groupName) + try container.encode(groupName, forKey: .groupName) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/RadioBoxes.swift b/MVMCoreUI/Atomic/Atoms/Views/RadioBoxes.swift index 58090598..6fe66bc2 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/RadioBoxes.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/RadioBoxes.swift @@ -10,28 +10,31 @@ import Foundation open class RadioBoxes: View { - public let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + public var collectionView: CollectionView! + public var collectionViewHeight: NSLayoutConstraint! + private let boxWidth: CGFloat = 151.0 + private let boxHeight: CGFloat = 64.0 + private let itemSpacing: CGFloat = 8.0 + + private var delegateObject: MVMCoreUIDelegateObject? /// The models for the molecules. public var boxes: [RadioBoxModel]? - public var collectionViewHeight: NSLayoutConstraint? - private let boxWidth: Double = 151.0 - private let boxHeight: Double = 64.0 - private let itemSpacing: Double = 10.0 - private let leadingSpacing: Double = 0 - - public var selectedBox: RadioBoxModel? { - get{ - guard let selectedItem = collectionView.indexPathsForSelectedItems?.first else {return nil} - return boxes?[selectedItem.item] + + private var size: CGFloat? + + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any collection size changes + DispatchQueue.main.async { + self.collectionView.collectionViewLayout.invalidateLayout() } } - + // MARK: - MVMCoreViewProtocol open override func setupView() { super.setupView() - collectionView.dataSource = self - collectionView.delegate = self + collectionView = createCollectionView() addSubview(collectionView) NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView) collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300) @@ -41,62 +44,66 @@ open class RadioBoxes: View { // MARK: - MoleculeViewProtocol public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) + self.delegateObject = delegateObject + guard let radioBoxesModel = model as? RadioBoxesModel else { return } + boxes = radioBoxesModel.boxes + FormValidator.setupValidation(for: radioBoxesModel, delegate: delegateObject?.formHolderDelegate) + backgroundColor = radioBoxesModel.backgroundColor?.uiColor registerCells() - setupLayout(with: radioBoxesModel) - prepareMolecules(with: radioBoxesModel) + setHeight() collectionView.reloadData() } @objc override open func updateView(_ size: CGFloat) { - collectionView.collectionViewLayout.invalidateLayout() - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - self.setNeedsDisplay() - self.collectionView.layoutIfNeeded() - self.collectionView.reloadData() - guard let firstSelectedIndex = self.boxes?.firstIndex(where: {$0.selected == true}) else { - return - } - self.collectionView.selectItem(at: IndexPath(item: firstSelectedIndex, section: 0), animated: true, scrollPosition: .centeredHorizontally) - - } - } - - // MARK: - JSON Setters - /// Updates the layout being used - - func setupLayout(with radioBoxesModel: RadioBoxesModel?) { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .vertical - layout.sectionInset = UIEdgeInsets.init(top: CGFloat(leadingSpacing), left: CGFloat(leadingSpacing), bottom: CGFloat(leadingSpacing), right: CGFloat(leadingSpacing)) - layout.minimumLineSpacing = 10 - layout.minimumInteritemSpacing = 10 - collectionView.collectionViewLayout = layout + super.updateView(size) + self.size = size + collectionView.updateView(size) } - func prepareMolecules(with radioBoxesModel: RadioBoxesModel?) { - guard let newMolecules = radioBoxesModel?.boxes else { - boxes = nil - return - } - boxes = newMolecules - let height = Double(round(Double((boxes?.count ?? Int(0.0)))/2.0))*(boxHeight+10.0) - collectionViewHeight?.constant = CGFloat(height) - collectionViewHeight?.isActive = true + // MARK: - Creation + + /// Creates the layout for the collection. + open func createCollectionViewLayout() -> UICollectionViewLayout { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.minimumLineSpacing = itemSpacing + layout.minimumInteritemSpacing = itemSpacing + return layout + } + + /// Creates the collection view. + open func createCollectionView() -> CollectionView { + let collection = CollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout()) + collection.dataSource = self + collection.delegate = self + return collection } /// Registers the cells with the collection view - func registerCells() { + open func registerCells() { collectionView.register(RadioBoxCollectionViewCell.self, forCellWithReuseIdentifier: "RadioBoxCollectionViewCell") } + + // MARK: - JSON Setters + open func setHeight() { + guard let boxes = boxes, boxes.count > 0 else { + collectionViewHeight.constant = 0 + return + } + + // Calculate the height + let rows = ceil(CGFloat(boxes.count) / 2.0) + let height = (rows * boxHeight) + ((rows - 1) * itemSpacing) + collectionViewHeight?.constant = height + } } extension RadioBoxes: UICollectionViewDelegateFlowLayout { open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - let itemWidth = (Double(collectionView.bounds.width) - itemSpacing)/2 - return CGSize(width: CGFloat(itemWidth), height: CGFloat(boxHeight)) + let itemWidth: CGFloat = (collectionView.bounds.width - itemSpacing) / 2 + return CGSize(width: itemWidth, height: boxHeight) } } @@ -106,23 +113,31 @@ extension RadioBoxes: UICollectionViewDataSource { } open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let molecule = boxes?[indexPath.row], let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RadioBoxCollectionViewCell", for: indexPath) as? RadioBoxCollectionViewCell else { - return UICollectionViewCell() + guard let molecule = boxes?[indexPath.row], + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RadioBoxCollectionViewCell", for: indexPath) as? RadioBoxCollectionViewCell else { + fatalError() + } + cell.radioBox.isUserInteractionEnabled = false + cell.set(with: molecule, delegateObject, nil) + cell.updateView(size ?? collectionView.bounds.width) + if molecule.selected { + collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically) } - cell.set(with: molecule, nil, nil) - cell.updateView(collectionView.bounds.width) cell.layoutIfNeeded() return cell } } + extension RadioBoxes: UICollectionViewDelegate { public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard let boxItem = boxes?[indexPath.row] else { return } - boxItem.selected = true + guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return } + cell.radioBox.selectBox() + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) } + public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - guard let boxItem = boxes?[indexPath.row] else { return } - boxItem.selected = false + guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return } + cell.radioBox.deselectBox() } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/RadioBoxesModel.swift b/MVMCoreUI/Atomic/Atoms/Views/RadioBoxesModel.swift index 653eae13..28c4fab5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/RadioBoxesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/RadioBoxesModel.swift @@ -7,13 +7,22 @@ // import Foundation -@objcMembers public class RadioBoxesModel: MoleculeModelProtocol { +@objcMembers public class RadioBoxesModel: MoleculeModelProtocol, FormFieldProtocol { public static var identifier: String = "radioBoxes" public var backgroundColor: Color? public var selectedAccentColor: Color? public var boxes: [RadioBoxModel] public var fieldKey: String? - public var groupName: String? + public var groupName: String = FormValidator.defaultGroupName + public var baseValue: AnyHashable? + + /// Returns the fieldValue of the selected box, otherwise the text of the selected box. + public func formFieldValue() -> AnyHashable? { + let selectedBox = boxes.first { (box) -> Bool in + return box.selected + } + return selectedBox?.fieldValue ?? selectedBox?.text + } private enum CodingKeys: String, CodingKey { case moleculeName @@ -30,7 +39,10 @@ import Foundation backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) boxes = try typeContainer.decode([RadioBoxModel].self, forKey: .boxes) fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) - groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) + if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + self.groupName = groupName + } + baseValue = formFieldValue() } public func encode(to encoder: Encoder) throws { @@ -40,6 +52,6 @@ import Foundation try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) - try container.encodeIfPresent(groupName, forKey: .groupName) + try container.encode(groupName, forKey: .groupName) } } diff --git a/MVMCoreUI/Atomic/Organisms/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel.swift index e900dda8..820c5a5d 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel.swift @@ -10,7 +10,7 @@ import UIKit open class Carousel: View { - public let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + public let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) /// The current index of the collection view. Includes dummy cells when looping. public var currentIndex = 0 diff --git a/MVMCoreUI/BaseClasses/CollectionView.swift b/MVMCoreUI/BaseClasses/CollectionView.swift index ff572a2e..a1a57376 100644 --- a/MVMCoreUI/BaseClasses/CollectionView.swift +++ b/MVMCoreUI/BaseClasses/CollectionView.swift @@ -39,6 +39,7 @@ open class CollectionView: UICollectionView, MVMCoreViewProtocol { public func setupView() { translatesAutoresizingMaskIntoConstraints = false showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false backgroundColor = .clear isAccessibilityElement = false contentInsetAdjustmentBehavior = .always