diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 294f6188..befe232c 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -190,8 +190,12 @@ AA1EC59924373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA1EC59824373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift */; }; AA56A20F243C5EE900303286 /* ListTwoColumnSubsectionDividerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA56A20E243C5EE900303286 /* ListTwoColumnSubsectionDividerModel.swift */; }; AA56A211243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA56A210243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift */; }; + AA85236C244435A20059CC1E /* RadioSwatchCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA85236B244435A20059CC1E /* RadioSwatchCollectionViewCell.swift */; }; AAA74A172410C04600080241 /* HeadersH2NoButtonsBodyText.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA74A162410C04600080241 /* HeadersH2NoButtonsBodyText.swift */; }; AAA74A192410C05800080241 /* HeadersH2NoButtonsBodyTextModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA74A182410C05800080241 /* HeadersH2NoButtonsBodyTextModel.swift */; }; + AAB9C10824346F4B00151545 /* RadioSwatches.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB9C10724346F4B00151545 /* RadioSwatches.swift */; }; + AAB9C10A243496DD00151545 /* RadioSwatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB9C109243496DD00151545 /* RadioSwatch.swift */; }; + AAC6F167243332E400F295C1 /* RadioSwatchesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC6F166243332E400F295C1 /* RadioSwatchesModel.swift */; }; BB2C968F24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2C968D24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift */; }; BB2C969224330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2C969124330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift */; }; BB47A586241615EF002BB23C /* ListOneColumnFullWidthTextDividerSubsectionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB47A585241615EF002BB23C /* ListOneColumnFullWidthTextDividerSubsectionModel.swift */; }; @@ -222,6 +226,7 @@ C7F8012323E846C300396FBD /* ListRVWheelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F8012223E846C300396FBD /* ListRVWheelModel.swift */; }; D202AFE4242A5F5E00E5BEDF /* NSTextAlignment+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D202AFE3242A5F5E00E5BEDF /* NSTextAlignment+Extension.swift */; }; D202AFE6242A6A9C00E5BEDF /* UICollectionViewScrollPosition+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D202AFE5242A6A9C00E5BEDF /* UICollectionViewScrollPosition+Extension.swift */; }; + D2092349244A51D40044AD09 /* RadioSwatchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2092348244A51D40044AD09 /* RadioSwatchModel.swift */; }; D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */; }; D20FB165241A5D75004AFC3A /* NavigationItemModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FB164241A5D75004AFC3A /* NavigationItemModelProtocol.swift */; }; D213347723843825008E41B3 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213347623843825008E41B3 /* Line.swift */; }; @@ -616,8 +621,12 @@ AA1EC59824373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListThreeColumnSpeedTestDivider.swift; sourceTree = ""; }; AA56A20E243C5EE900303286 /* ListTwoColumnSubsectionDividerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTwoColumnSubsectionDividerModel.swift; sourceTree = ""; }; AA56A210243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTwoColumnSubsectionDivider.swift; sourceTree = ""; }; + AA85236B244435A20059CC1E /* RadioSwatchCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSwatchCollectionViewCell.swift; sourceTree = ""; }; AAA74A162410C04600080241 /* HeadersH2NoButtonsBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadersH2NoButtonsBodyText.swift; sourceTree = ""; }; AAA74A182410C05800080241 /* HeadersH2NoButtonsBodyTextModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadersH2NoButtonsBodyTextModel.swift; sourceTree = ""; }; + AAB9C10724346F4B00151545 /* RadioSwatches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSwatches.swift; sourceTree = ""; }; + AAB9C109243496DD00151545 /* RadioSwatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSwatch.swift; sourceTree = ""; }; + AAC6F166243332E400F295C1 /* RadioSwatchesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSwatchesModel.swift; sourceTree = ""; }; BB2C968D24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListRightVariableTextLinkAllTextAndLinksModel.swift; sourceTree = ""; }; BB2C969124330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListRightVariableTextLinkAllTextAndLinks.swift; sourceTree = ""; }; BB47A585241615EF002BB23C /* ListOneColumnFullWidthTextDividerSubsectionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextDividerSubsectionModel.swift; sourceTree = ""; }; @@ -648,6 +657,7 @@ C7F8012223E846C300396FBD /* ListRVWheelModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRVWheelModel.swift; sourceTree = ""; }; D202AFE3242A5F5E00E5BEDF /* NSTextAlignment+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSTextAlignment+Extension.swift"; sourceTree = ""; }; D202AFE5242A6A9C00E5BEDF /* UICollectionViewScrollPosition+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewScrollPosition+Extension.swift"; sourceTree = ""; }; + D2092348244A51D40044AD09 /* RadioSwatchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioSwatchModel.swift; sourceTree = ""; }; D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoButtonView.swift; sourceTree = ""; }; D20FB164241A5D75004AFC3A /* NavigationItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModelProtocol.swift; sourceTree = ""; }; D213347623843825008E41B3 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -1429,6 +1439,11 @@ 01004F2F22721C3800991ECC /* RadioButton.swift */, 31BE15CA23D8924C00452370 /* CheckboxModel.swift */, 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */, + AAC6F166243332E400F295C1 /* RadioSwatchesModel.swift */, + AAB9C10724346F4B00151545 /* RadioSwatches.swift */, + D2092348244A51D40044AD09 /* RadioSwatchModel.swift */, + AAB9C109243496DD00151545 /* RadioSwatch.swift */, + AA85236B244435A20059CC1E /* RadioSwatchCollectionViewCell.swift */, ); path = Selectors; sourceTree = ""; @@ -2040,6 +2055,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AAC6F167243332E400F295C1 /* RadioSwatchesModel.swift in Sources */, 5248BFED23F12E350059236A /* ListThreeColumnPlanDataDividerModel.swift in Sources */, 0A5D59C223AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift in Sources */, 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */, @@ -2056,6 +2072,7 @@ D29B771022C281F400D6ACE0 /* ModuleMolecule.swift in Sources */, D28A838923CCCFCB00DFE4FC /* LinkModel.swift in Sources */, AA56A20F243C5EE900303286 /* ListTwoColumnSubsectionDividerModel.swift in Sources */, + AAB9C10824346F4B00151545 /* RadioSwatches.swift in Sources */, 94C2D9A923872E5E0006CF46 /* LabelAttributeImageModel.swift in Sources */, DBC4391922442197001AB423 /* DashLine.swift in Sources */, D264FAAA2440F97600D98315 /* CollectionView.swift in Sources */, @@ -2100,6 +2117,7 @@ 012A88AD238C418100FE3DA1 /* TemplateProtocol.swift in Sources */, BB6C6AC1242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift in Sources */, D21B7F77243BB70700051ABF /* MoleculeCollectionItemModel.swift in Sources */, + AAB9C10A243496DD00151545 /* RadioSwatch.swift in Sources */, D29DF2B421E7B76D003B2FB9 /* MFLoadingSpinner.m in Sources */, 011D9602240DA20A000E3791 /* FormRuleWatcherFieldProtocol.swift in Sources */, D264FAA1243CF66B00D98315 /* ContainerCollectionReusableView.swift in Sources */, @@ -2204,6 +2222,7 @@ D260105D23D0BCD400764D80 /* Stack.swift in Sources */, 0A7EF85D23D8A95600B2AAD1 /* TextEntryFieldModel.swift in Sources */, BB54C5212434D92F0038326C /* ListRightVariableButtonAllTextAndLinksModel.swift in Sources */, + D2092349244A51D40044AD09 /* RadioSwatchModel.swift in Sources */, 8DD1E370243B3D0500D8F2DF /* ListThreeColumnInternationalData.swift in Sources */, D2D6CD4222E78FAB00D701B8 /* ThreeLayerTemplate.swift in Sources */, 01EB368F23609801006832FA /* LabelModel.swift in Sources */, @@ -2318,6 +2337,7 @@ 0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */, AA56A211243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift in Sources */, D264FA8C243BCD8E00D98315 /* CollectionTemplateModel.swift in Sources */, + AA85236C244435A20059CC1E /* RadioSwatchCollectionViewCell.swift in Sources */, 52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */, D26C5A6B23F4A40D007AEECE /* ListItemModel.swift in Sources */, 0A21DB8D235E06EF00C160A2 /* MFDigitTextField.m in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift new file mode 100644 index 00000000..35294457 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift @@ -0,0 +1,166 @@ +// +// RadioSwatch.swift +// MVMCoreUI +// +// Created by Lekshmi S on 01/04/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation +import UIKit + +open class RadioSwatch: Control { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public let bottomText = Label.createLabelRegularMicro(true) + + private var circleLayer: CAShapeLayer? + private var selectedLayer: CALayer? + private var strikeLayer: CALayer? + private var maskLayer: CALayer? + + public var radioSwatchModel: RadioSwatchModel? { + return model as? RadioSwatchModel + } + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func updateView(_ size: CGFloat) { + super.updateView(size) + bottomText.updateView(size) + layer.setNeedsDisplay() + } + + open override func setupView() { + super.setupView() + + addSubview(bottomText) + bottomText.textAlignment = .center + NSLayoutConstraint.constraintPinSubview(bottomText, pinTop: true, topConstant: 38, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0) + addTarget(self, action: #selector(selectSwatch), for: .touchUpInside) + } + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + guard let model = model as? RadioSwatchModel else { return } + isSelected = model.selected + isEnabled = model.enabled + bottomText.text = model.text + } + + public override func reset() { + super.reset() + isSelected = false + isEnabled = true + bottomText.text = nil + } + + //------------------------------------------------------ + // MARK: - State Handling + //------------------------------------------------------ + open override func draw(_ layer: CALayer, in ctx: CGContext) { + //Draw the swatch + circleLayer?.removeFromSuperlayer() + let circle = getCircle(color: isSelected ? .mvmBlack : .mvmCoolGray6, thickness: 1) + layer.addSublayer(circle) + circleLayer = circle + + //Draw the strikethrough + strikeLayer?.removeFromSuperlayer() + if radioSwatchModel?.strikethrough ?? false { + let line = getStrikeThrough(color: isSelected ? .mvmBlack : .mvmCoolGray6, thickness: 1) + layer.addSublayer(line) + strikeLayer = line + } + + //Draw the selected layer + selectedLayer?.removeFromSuperlayer() + if isSelected { + let outerCircle = getSelectedLayer(color: isSelected ? .mvmBlack : .mvmCoolGray6, thickness: 1) + layer.addSublayer(outerCircle) + selectedLayer = outerCircle + bottomText.isHidden = false + } else { + bottomText.isHidden = true + } + + //Handle Mask + maskLayer?.removeFromSuperlayer() + if !isEnabled { + let mask = getMaskLayer() + layer.mask = mask + maskLayer = mask + } + } + + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any size changes + layer.setNeedsDisplay() + } + + @objc open func selectSwatch() { + guard isEnabled else { return } + isSelected = true + radioSwatchModel?.selected = isSelected + layer.setNeedsDisplay() + } + + @objc open func deselectSwatch() { + isSelected = false + radioSwatchModel?.selected = isSelected + layer.setNeedsDisplay() + } + + func getCircle(color: UIColor, thickness: CGFloat) -> CAShapeLayer { + let circle = CAShapeLayer() + circle.name = "innercircle" + circle.fillColor = radioSwatchModel?.color.cgColor ?? UIColor.blue.cgColor + circle.opacity = 1.0 + circle.lineWidth = thickness + circle.strokeColor = color.cgColor + + let circlePath = UIBezierPath(ovalIn: CGRect(x: 12, y: 1, width: 30, height: 30)) + circle.path = circlePath.cgPath + return circle + } + + func getStrikeThrough(color: UIColor, thickness: CGFloat) -> CAShapeLayer { + let strikeThrough = CAShapeLayer() + strikeThrough.name = "strikethrough" + strikeThrough.fillColor = nil + strikeThrough.opacity = 1.0 + strikeThrough.lineWidth = thickness + strikeThrough.strokeColor = color.cgColor + + let linePath = UIBezierPath() + linePath.move(to: CGPoint(x: 12, y: 31)) + linePath.addLine(to: CGPoint(x: 42, y: 1)) + strikeThrough.path = linePath.cgPath + return strikeThrough + } + + func getSelectedLayer(color: UIColor, thickness: CGFloat) -> CAShapeLayer { + circleLayer?.path = UIBezierPath(ovalIn: CGRect(x: 15, y: 4, width: 24, height: 24)).cgPath + let circle = CAShapeLayer() + circle.name = "outercircle" + circle.fillColor = UIColor.clear.cgColor + circle.opacity = 1.0 + circle.lineWidth = thickness + circle.strokeColor = color.cgColor + + let circlePath = UIBezierPath(ovalIn: CGRect(x: 12, y: 1, width: 30, height: 30)) + circle.path = circlePath.cgPath + return circle + } + + func getMaskLayer() -> CALayer { + let mask = CALayer() + mask.backgroundColor = UIColor.white.cgColor + mask.opacity = 0.3 + mask.frame = bounds + return mask + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift new file mode 100644 index 00000000..8412dab2 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift @@ -0,0 +1,23 @@ +// +// RadioSwatchCollectionViewCell.swift +// MVMCoreUI +// +// Created by Lekshmi S on 13/04/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation +open class RadioSwatchCollectionViewCell: CollectionViewCell { + public let radioSwatch = RadioSwatch() + + open override func setupView() { + super.setupView() + addMolecule(radioSwatch) + MVMCoreUIUtility.setMarginsFor(contentView, leading: 0, top: 0, trailing: 0, bottom: 0) + } + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + guard let model = model as? RadioSwatchModel else { return } + radioSwatch.set(with: model, delegateObject, additionalData) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift new file mode 100644 index 00000000..3aadd467 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift @@ -0,0 +1,64 @@ +// +// RadioSwatchModel.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 4/17/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers public class RadioSwatchModel: MoleculeModelProtocol { + public static var identifier: String = "radioSwatch" + public var backgroundColor: Color? + public var color: Color = Color(uiColor: .mvmBlue) + public var text: String? + public var selected: Bool = false + public var enabled: Bool = true + public var strikethrough: Bool = false + public var fieldValue: String? + + private enum CodingKeys: String, CodingKey { + case moleculeName + case backgroundColor + case color + case text + case selected + case enabled + case strikethrough + case fieldValue + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) { + self.color = color + } + text = try typeContainer.decodeIfPresent(String.self, forKey: .text) + if let selected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) { + self.selected = selected + } + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { + self.enabled = enabled + } + if let strikethrough = try typeContainer.decodeIfPresent(Bool.self, forKey: .strikethrough) { + self.strikethrough = strikethrough + } + fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encode(color, forKey: .color) + try container.encodeIfPresent(text, forKey: .text) + try container.encode(selected, forKey: .selected) + try container.encode(enabled, forKey: .enabled) + try container.encode(strikethrough, forKey: .strikethrough) + try container.encodeIfPresent(fieldValue, forKey: .fieldValue) + } +} + + diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift new file mode 100644 index 00000000..8a6b8faf --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift @@ -0,0 +1,152 @@ +// +// RadioSwatches.swift +// MVMCoreUI +// +// Created by Lekshmi S on 01/04/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +open class RadioSwatches: View { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var collectionView: CollectionView! + public var swatches: [RadioSwatchModel]? + private var size: CGFloat? + private var delegateObject: MVMCoreUIDelegateObject? + + //------------------------------------------------------ + // MARK: - Constraints + //------------------------------------------------------ + public var collectionViewHeight: NSLayoutConstraint? + private let cellSize: CGFloat = 54.0 + private let itemSpacing: CGFloat = 8.0 + + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any collection size changes + setHeight() + DispatchQueue.main.async { + self.collectionView.collectionViewLayout.invalidateLayout() + } + } + + open override func setupView() { + super.setupView() + collectionView = createCollectionView() + addSubview(collectionView) + NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView) + collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 100) + collectionViewHeight?.isActive = true + } + + @objc override open func updateView(_ size: CGFloat) { + super.updateView(size) + self.size = size + collectionView.updateView(size) + } + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + self.delegateObject = delegateObject + + guard let radioSwatchesModel = model as? RadioSwatchesModel else { return } + swatches = radioSwatchesModel.swatches + FormValidator.setupValidation(for: radioSwatchesModel, delegate: delegateObject?.formHolderDelegate) + collectionView.reloadData() + } + + //------------------------------------------------------ + // MARK: - Methods + //------------------------------------------------------ + /// Creates the collection view. + open func createCollectionView() -> CollectionView { + let collection = CollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout()) + collection.dataSource = self + collection.delegate = self + collection.register(RadioSwatchCollectionViewCell.self, forCellWithReuseIdentifier: "RadioSwatchCollectionViewCell") + return collection + } + + /// Creates the layout for the collection. + open func createCollectionViewLayout() -> UICollectionViewLayout { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .vertical + layout.minimumLineSpacing = itemSpacing + layout.minimumInteritemSpacing = itemSpacing + return layout + } + + open func setHeight() { + guard let swatches = swatches, 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)) + + if let oldHeight = collectionViewHeight?.constant, + height != oldHeight { + // Notify delegate of height change, called async to avoid various race conditions caused while happening while laying out initially. + DispatchQueue.main.async { + self.delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self) + } + } + collectionViewHeight?.constant = CGFloat(height) + } +} + +//------------------------------------------------------ +// MARK: - Delegate methods +//------------------------------------------------------ +extension RadioSwatches: UICollectionViewDelegateFlowLayout { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + return CGSize(width: cellSize, height: cellSize) + } +} + +extension RadioSwatches: UICollectionViewDataSource { + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return swatches?.count ?? 0 + } + + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + guard let molecule = swatches?[indexPath.row], let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RadioSwatchCollectionViewCell", for: indexPath) as? RadioSwatchCollectionViewCell else { + fatalError() + } + cell.reset() + cell.radioSwatch.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.layoutIfNeeded() + return cell + } +} + +extension RadioSwatches: UICollectionViewDelegate { + open func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { + guard let molecule = swatches?[indexPath.row] else { return false } + return molecule.enabled + } + + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let cell = collectionView.cellForItem(at: indexPath) as? RadioSwatchCollectionViewCell else { return } + cell.radioSwatch.selectSwatch() + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + } + + open func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + guard let cell = collectionView.cellForItem(at: indexPath) as? RadioSwatchCollectionViewCell else { return } + cell.radioSwatch.deselectSwatch() + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift new file mode 100644 index 00000000..db8e5d5d --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift @@ -0,0 +1,54 @@ +// +// RadioSwatchesModel.swift +// MVMCoreUI +// +// Created by Lekshmi S on 31/03/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objcMembers public class RadioSwatchesModel: MoleculeModelProtocol, FormFieldProtocol { + public static var identifier: String = "radioSwatches" + public var backgroundColor: Color? + public var swatches: [RadioSwatchModel] + public var fieldKey: String? + public var groupName: String = FormValidator.defaultGroupName + public var baseValue: AnyHashable? + + /// Returns the fieldValue of the selected swatch, otherwise the text of selected swatch. + public func formFieldValue() -> AnyHashable? { + let selectedSwatch = swatches.first { (swatch) -> Bool in + return swatch.selected + } + return selectedSwatch?.fieldValue ?? selectedSwatch?.text + } + + private enum CodingKeys: String, CodingKey { + case moleculeName + case backgroundColor + case swatches + case fieldKey + case groupName + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + swatches = try typeContainer.decode([RadioSwatchModel].self, forKey: .swatches) + fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) + if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { + self.groupName = groupName + } + baseValue = formFieldValue() + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encode(swatches, forKey: .swatches) + try container.encodeIfPresent(fieldKey, forKey: .fieldKey) + try container.encode(groupName, forKey: .groupName) + } +} diff --git a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift index ab0d5b43..1b050aca 100644 --- a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift +++ b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift @@ -70,6 +70,7 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: RadioButton.self, viewModelClass: RadioButtonModel.self) MoleculeObjectMapping.shared()?.register(viewClass: RadioBoxes.self, viewModelClass: RadioBoxesModel.self) MoleculeObjectMapping.shared()?.register(viewClass: Checkbox.self, viewModelClass: CheckboxModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: RadioSwatches.self, viewModelClass: RadioSwatchesModel.self) // Other Atoms MoleculeObjectMapping.shared()?.register(viewClass: ProgressBar.self, viewModelClass: ProgressBarModel.self)