diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index f7f5870b..fbb120aa 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -594,6 +594,14 @@ EA6642932BCDA97D00D81DC4 /* TileContainerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */; }; EA6E8B952B504A43000139B4 /* ButtonGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B942B504A43000139B4 /* ButtonGroup.swift */; }; EA6E8B972B504A4D000139B4 /* ButtonGroupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA6E8B962B504A4D000139B4 /* ButtonGroupModel.swift */; }; + EA7AE5472C73C01A00107C74 /* CheckboxesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5462C73C01A00107C74 /* CheckboxesModel.swift */; }; + EA7AE5492C7403DC00107C74 /* Checkboxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5482C7403DC00107C74 /* Checkboxes.swift */; }; + EA7AE54B2C74CACA00107C74 /* RadioButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE54A2C74CACA00107C74 /* RadioButtons.swift */; }; + EA7AE54D2C74CAD700107C74 /* RadioButtonsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE54C2C74CAD700107C74 /* RadioButtonsModel.swift */; }; + EA7AE54F2C74EB3700107C74 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE54E2C74EB3700107C74 /* CalendarView.swift */; }; + EA7AE5512C74EB4500107C74 /* CalendarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */; }; + EA7AE5532C74F1F600107C74 /* DatePickerEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */; }; + EA7AE5552C74F20600107C74 /* DatePickerEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */; }; EA7D81602B2B6E6800D29F9E /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D815F2B2B6E6800D29F9E /* Icon.swift */; }; EA7D81622B2B6E7F00D29F9E /* IconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D81612B2B6E7F00D29F9E /* IconModel.swift */; }; EA7D81642B2BABCB00D29F9E /* TooltipModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */; }; @@ -1223,6 +1231,14 @@ EA6642922BCDA97D00D81DC4 /* TileContainerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileContainerModel.swift; sourceTree = ""; }; EA6E8B942B504A43000139B4 /* ButtonGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroup.swift; sourceTree = ""; }; EA6E8B962B504A4D000139B4 /* ButtonGroupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupModel.swift; sourceTree = ""; }; + EA7AE5462C73C01A00107C74 /* CheckboxesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckboxesModel.swift; sourceTree = ""; }; + EA7AE5482C7403DC00107C74 /* Checkboxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkboxes.swift; sourceTree = ""; }; + EA7AE54A2C74CACA00107C74 /* RadioButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtons.swift; sourceTree = ""; }; + EA7AE54C2C74CAD700107C74 /* RadioButtonsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButtonsModel.swift; sourceTree = ""; }; + EA7AE54E2C74EB3700107C74 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; + EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarViewModel.swift; sourceTree = ""; }; + EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerEntryField.swift; sourceTree = ""; }; + EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerEntryFieldModel.swift; sourceTree = ""; }; EA7D815F2B2B6E6800D29F9E /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; EA7D81612B2B6E7F00D29F9E /* IconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconModel.swift; sourceTree = ""; }; EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipModel.swift; sourceTree = ""; }; @@ -2031,8 +2047,12 @@ BBAA4F00243D8E3B005AAD5F /* RadioBoxModel.swift */, D264FAA6243FE13B00D98315 /* RadioBox.swift */, 0116A4E4228B19640094F3ED /* RadioButtonSelectionHelper.swift */, + EA7AE54C2C74CAD700107C74 /* RadioButtonsModel.swift */, + EA7AE54A2C74CACA00107C74 /* RadioButtons.swift */, 011D95AE2407266E000E3791 /* RadioButtonModel.swift */, 01004F2F22721C3800991ECC /* RadioButton.swift */, + EA7AE5462C73C01A00107C74 /* CheckboxesModel.swift */, + EA7AE5482C7403DC00107C74 /* Checkboxes.swift */, 31BE15CA23D8924C00452370 /* CheckboxModel.swift */, 0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */, AAC6F166243332E400F295C1 /* RadioSwatchesModel.swift */, @@ -2362,6 +2382,8 @@ EA7D815F2B2B6E6800D29F9E /* Icon.swift */, EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */, EA7D81652B2BABD200D29F9E /* Tooltip.swift */, + EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */, + EA7AE54E2C74EB3700107C74 /* CalendarView.swift */, ); path = Views; sourceTree = ""; @@ -2513,6 +2535,8 @@ D2BEFEF5248A954C00FAB3A9 /* FormFields */ = { isa = PBXGroup; children = ( + EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */, + EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */, EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */, D2BEFEF6248A957A00FAB3A9 /* Tags */, D29DF22B21E6A0FA003B2FB9 /* TextFields */, @@ -2848,6 +2872,7 @@ AA7F32AD246C0F8C00C965BA /* ListLeftVariableRadioButtonAllTextAndLinks.swift in Sources */, EAA482CE2B45F2F300978105 /* MFLoadingSpinner+VDS.swift in Sources */, D272F5F92473163100BD1A8F /* BarButtonItem.swift in Sources */, + EA7AE54D2C74CAD700107C74 /* RadioButtonsModel.swift in Sources */, D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */, 0A9D09202433796500D2E6C0 /* BarsIndicatorView.swift in Sources */, D2E2A99423D8CCBC000B42E6 /* HeadlineBodyLinkModel.swift in Sources */, @@ -2862,6 +2887,7 @@ EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */, 011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */, EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */, + EA7AE5532C74F1F600107C74 /* DatePickerEntryField.swift in Sources */, EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */, BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */, D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */, @@ -3108,7 +3134,9 @@ AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */, AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */, 58A9DD7D2AC2103300F5E0B0 /* ReplaceableMoleculeBehaviorModel.swift in Sources */, + EA7AE54B2C74CACA00107C74 /* RadioButtons.swift in Sources */, D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */, + EA7AE5552C74F20600107C74 /* DatePickerEntryFieldModel.swift in Sources */, D29DF27A21E7A533003B2FB9 /* MVMCoreUISession.m in Sources */, 27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */, EA6E8B972B504A4D000139B4 /* ButtonGroupModel.swift in Sources */, @@ -3265,6 +3293,7 @@ 01F2C20327C81F9700DC3D36 /* SubNavManagerNavigationController.swift in Sources */, EABFC1412763BB8D00E78B40 /* FormLabel.swift in Sources */, AA997252247530B100FC7472 /* ListLeftVariableIconAllTextLinks.swift in Sources */, + EA7AE5492C7403DC00107C74 /* Checkboxes.swift in Sources */, D2A5146122121FBF00345BFB /* MoleculeStackTemplate.swift in Sources */, D23EA7FB2475F09800D60C34 /* CarouselItemProtocol.swift in Sources */, D2E2A9A323E096B1000B42E6 /* DisableableModelProtocol.swift in Sources */, @@ -3295,6 +3324,7 @@ BB6C6AC824225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift in Sources */, 0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */, D21B7F73243BAC6800051ABF /* CollectionItemModelProtocol.swift in Sources */, + EA7AE5512C74EB4500107C74 /* CalendarViewModel.swift in Sources */, AA104B1A24474A66004D2810 /* HeadersH2Buttons.swift in Sources */, C7192E7D23C301750050C2A0 /* HeadLineBodyCaretLinkImage.swift in Sources */, D2D2FCF0252B72AF0033EAAA /* MoleculeSectionFooterModel.swift in Sources */, @@ -3315,6 +3345,7 @@ D2D3957D252FDBCD00047B11 /* ModalSectionListTemplateModel.swift in Sources */, D2B9D0E4265EEE9D0084735C /* MoleculeListProtocol.swift in Sources */, D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */, + EA7AE5472C73C01A00107C74 /* CheckboxesModel.swift in Sources */, 8D4687E2242E2DE400802879 /* ListFourColumnDataUsageListItemModel.swift in Sources */, D29E28DD23D7404C00ACEA85 /* ContainerHelper.swift in Sources */, EA6642912BCDA97300D81DC4 /* TileContainer.swift in Sources */, @@ -3355,6 +3386,7 @@ D2169301251E51E7002A6324 /* SectionListTemplate.swift in Sources */, 0A6682AA2435125F00AD3CA1 /* Styler.swift in Sources */, D264FAA7243FE13B00D98315 /* RadioBox.swift in Sources */, + EA7AE54F2C74EB3700107C74 /* CalendarView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryField.swift new file mode 100644 index 00000000..fd65ae92 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryField.swift @@ -0,0 +1,51 @@ +// +// DatePickerEntryField.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class DatePickerEntryField: VDS.DatePicker, VDSMoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var viewModel: DatePickerEntryFieldModel! + public var delegateObject: MVMCoreUIDelegateObject? + public var additionalData: [AnyHashable : Any]? + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + open override func setup() { + super.setup() + //turn off internal required rule + useRequiredRule = false + + publisher(for: .valueChanged) + .sink { [weak self] control in + guard let self, let viewModel, isEnabled else { return } + viewModel.selectedDate = control.selectedDate + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + }.store(in: &subscribers) + } + + public func viewModelDidUpdate() { + surface = viewModel.surface + labelText = viewModel.title + helperText = viewModel.feedback + helperTextPlacement = viewModel.feedbackTextPlacement + tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel() + transparentBackground = viewModel.transparentBackground + width = viewModel.width + selectedDate = viewModel.selectedDate + calendarModel = viewModel.calendar.convertToVDSCalendarModel() + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + } + + public func updateView(_ size: CGFloat) {} + +} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryFieldModel.swift new file mode 100644 index 00000000..f7f84b07 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryFieldModel.swift @@ -0,0 +1,121 @@ +// +// DatePickerEntryFieldModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class DatePickerEntryFieldModel: FormFieldModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public override static var identifier: String { "datePicker" } + + public var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeZone = NSTimeZone.system + formatter.locale = .current + formatter.formatterBehavior = .default + return formatter + }() + + /// Update the property value to alter the format of how the date is presented. + public var dateFormat: String = "MMM d, y" { + didSet { dateFormatter.dateFormat = dateFormat } + } + + public var selectedDate: Date? + public var calendar: CalendarViewModel = .init() + public var title: String? + public var feedback: String? + public var feedbackTextPlacement: VDS.DatePicker.HelperTextPlacement = .bottom + public var tooltip: TooltipModel? + public var transparentBackground: Bool = false + public var width: CGFloat? + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { + case dateFormat + case selectedDate + case calendar + case title + case feedback + case feedbackTextPlacement + case tooltip + case transparentBackground + case width + } + + //-------------------------------------------------- + // MARK: - Form Validation + //-------------------------------------------------- + + /// Returns the fieldValue of the selectedDate. + public override func formFieldValue() -> AnyHashable? { + guard let selectedDate, enabled else { return nil } + return dateFormatter.string(from: selectedDate) + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) { + self.dateFormat = dateFormat + dateFormatter.dateFormat = dateFormat + } + + if let date = try container.decodeIfPresent(String.self, forKey: .selectedDate) { + selectedDate = calendar.dateFormatter.date(from: date) + } + + if let calendar = try container.decodeIfPresent(CalendarViewModel.self, forKey: .calendar) { + self.calendar = calendar + } + + title = try container.decodeIfPresent(String.self, forKey: .title) + feedback = try container.decodeIfPresent(String.self, forKey: .feedback) + feedbackTextPlacement = try container.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom + + tooltip = try container.decodeIfPresent(TooltipModel.self, forKey: .tooltip) + transparentBackground = try container.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false + width = try container.decodeIfPresent(CGFloat.self, forKey: .width) + + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(selectedDate, forKey: .selectedDate) + try container.encode(calendar, forKey: .calendar) + try container.encodeIfPresent(title, forKey: .title) + try container.encodeIfPresent(feedback, forKey: .feedback) + try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement) + try container.encodeIfPresent(tooltip, forKey: .tooltip) + try container.encode(transparentBackground, forKey: .transparentBackground) + try container.encodeIfPresent(width, forKey: .width) + } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return dateFormat == model.dateFormat + && selectedDate == model.selectedDate + && calendar == model.calendar + && title == model.title + && feedback == model.feedback + && feedbackTextPlacement == model.feedbackTextPlacement + && tooltip == model.tooltip + && transparentBackground == model.transparentBackground + && width == model.width + } +} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift index c96a8266..9f5c0415 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/FormFieldModel.swift @@ -15,6 +15,8 @@ import VDS // MARK: - Properties //-------------------------------------------------- public class var identifier: String { "" } + public var moleculeName: String { Self.identifier } + public var id: String = UUID().uuidString public var backgroundColor: Color? @@ -23,7 +25,7 @@ import VDS public var enabled: Bool = true public var required: Bool = true public var readOnly: Bool = false - public var showError: Bool? + public var showError: Bool = false public var errorMessage: String? public var initialErrorMessage: String? @@ -66,6 +68,7 @@ import VDS case moleculeName case accessibilityIdentifier case errorMessage + case showError case enabled case readOnly case required @@ -107,6 +110,7 @@ import VDS accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) initialErrorMessage = errorMessage + showError = try typeContainer.decodeIfPresent(Bool.self, forKey: .showError) ?? false enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false @@ -126,9 +130,30 @@ import VDS try container.encodeIfPresent(errorMessage, forKey: .errorMessage) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) + try container.encode(showError, forKey: .showError) try container.encode(readOnly, forKey: .readOnly) try container.encode(enabled, forKey: .enabled) try container.encode(required, forKey: .required) try container.encode(inverted, forKey: .inverted) } + + open func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return moleculeName == model.moleculeName + && enabled == model.enabled + && showError == model.showError + && errorMessage == model.errorMessage + && readOnly == model.readOnly + && required == model.required + && inverted == model.inverted + && fieldKey == model.fieldKey + && groupName == model.groupName + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && accessibilityTraits == model.accessibilityTraits + } +} + +extension FormFieldModel { + public var isEnabled: Bool { enabled && !readOnly } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift index 7025d18e..8861b50a 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift @@ -146,7 +146,7 @@ open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, isEnabled = viewModel.enabled isReadOnly = viewModel.readOnly isRequired = viewModel.required - tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel() width = viewModel.width transparentBackground = viewModel.transparentBackground diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift index 04d880ae..33512fc8 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift @@ -78,4 +78,13 @@ import VDS try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement) try container.encodeModelIfPresent(action, forKey: .action) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return options == model.options + && selectedIndex == model.selectedIndex + && showInlineLabel == model.showInlineLabel + && feedbackTextPlacement == model.feedbackTextPlacement + && action.isEqual(to: model.action) + } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift index 1ef00628..4ddb9a35 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift @@ -34,7 +34,6 @@ import Foundation //-------------------------------------------------- private enum CodingKeys: String, CodingKey { - case backgroundColor case title case feedback case errorTextColor @@ -86,7 +85,6 @@ import Foundation required public init(from decoder: Decoder) throws { try super.init(from: decoder) let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) title = try typeContainer.decodeIfPresent(String.self, forKey: .title) feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback) errorTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .errorTextColor) @@ -106,7 +104,6 @@ import Foundation open override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(feedback, forKey: .feedback) try container.encodeIfPresent(text, forKey: .text) @@ -116,4 +113,16 @@ import Foundation try container.encode(hideBorders, forKey: .hideBorders) try container.encode(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return selected == model.selected + && title == model.title + && feedback == model.feedback + && errorTextColor == model.errorTextColor + && locked == model.locked + && hideBorders == model.hideBorders + && text == model.text + && shouldMaskRecordedView == model.shouldMaskRecordedView + } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift index fb99a10a..476ee550 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/InputEntryField.swift @@ -173,7 +173,7 @@ import VDS isEnabled = viewModel.enabled isReadOnly = viewModel.readOnly isRequired = viewModel.required - tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel() width = viewModel.width transparentBackground = viewModel.transparentBackground diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift index cb40ae9b..7cbad244 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift @@ -218,4 +218,21 @@ import VDS try container.encode(transparentBackground, forKey: .transparentBackground) try container.encodeIfPresent(width, forKey: .width) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return placeholder == model.placeholder + && textAlignment == model.textAlignment + && enabledTextColor == model.enabledTextColor + && disabledTextColor == model.disabledTextColor + && keyboardOverride == model.keyboardOverride + && type == model.type + && clearTextOnTap == model.clearTextOnTap + && displayFormat == model.displayFormat + && displayMask == model.displayMask + && enableClipboardActions == model.enableClipboardActions + && tooltip == model.tooltip + && transparentBackground == model.transparentBackground + && width == model.width + } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift index bce7eb7b..e023df5d 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift @@ -126,7 +126,7 @@ open class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingT isEnabled = viewModel.enabled isReadOnly = viewModel.readOnly isRequired = viewModel.required - tooltipModel = viewModel.tooltip?.toVDSTooltipModel() + tooltipModel = viewModel.tooltip?.convertToVDSTooltipModel() width = viewModel.width transparentBackground = viewModel.transparentBackground diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift index 2f47b2a6..d123e7f8 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift @@ -21,7 +21,7 @@ public class TextViewEntryFieldModel: TextEntryFieldModel { public var showsPlaceholder: Bool = false public var minHeight: VDS.TextArea.Height = .twoX public var maxLength: Int? - + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- @@ -55,4 +55,13 @@ public class TextViewEntryFieldModel: TextEntryFieldModel { try container.encode(minHeight, forKey: .minHeight) try container.encodeIfPresent(maxLength, forKey: .maxLength) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return accessibilityText == model.accessibilityText + && editable == model.editable + && minHeight == model.minHeight + && maxLength == model.maxLength + } + } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift index 3e6e2a84..728a295b 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift @@ -131,7 +131,7 @@ import VDS } //properties - isEnabled = viewModel.enabled && !viewModel.readOnly + isEnabled = viewModel.isEnabled isAnimated = viewModel.animated //call super here to go around the didSet diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift index 00554c2e..4ae2c89e 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift @@ -94,4 +94,12 @@ import VDS try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(offAction, forKey: .offAction) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return selected == model.selected + && animated == model.animated + && offAction.isEqual(to: model.offAction) + && action.isEqual(to: model.action) + } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Checkboxes.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Checkboxes.swift new file mode 100644 index 00000000..91040f77 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Checkboxes.swift @@ -0,0 +1,55 @@ +// +// Checkboxes.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/19/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class Checkboxes: VDS.CheckboxGroup, VDSMoleculeViewProtocol { + + //------------------------------------------------------ + // MARK: - Properties + //------------------------------------------------------ + open var viewModel: CheckboxesModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + // Form Validation + var fieldKey: String? + var fieldValue: JSONValue? + var groupName: String? + + /// The models for the molecules. + public var checkboxes: [CheckboxLabelModel]? + + // MARK: - MoleculeViewProtocol + public func viewModelDidUpdate() { + surface = viewModel.surface + showError = viewModel.showError + isEnabled = viewModel.enabled && !viewModel.readOnly + checkboxes = viewModel.checkboxes + checkboxes?.forEach { + FormValidator.setupValidation(for: $0.checkbox, delegate: delegateObject?.formHolderDelegate) + } + + selectorModels = viewModel.checkboxes.convertToVDSCheckboxItemModel(surface: surface, + delegateObject: delegateObject, + additionalData: additionalData) + } + + open func updateView(_ size: CGFloat) {} + + open override func didSelect(_ selectedControl: CheckboxItem) { + super.didSelect(selectedControl) + + // since the checkboxes has the state being tracked, we need to update the values here. + if let index = items.firstIndex(where: {$0 === selectedControl}), let selected = checkboxes?[index] { + selected.checkbox.selected = true + } + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxesModel.swift new file mode 100644 index 00000000..08e9c030 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxesModel.swift @@ -0,0 +1,101 @@ +// +// CheckboxesModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/19/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore +import VDS + +public class CheckboxesModel: MoleculeModelProtocol, ParentMoleculeModelProtocol { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String { "checkboxes" } + public var id: String = UUID().uuidString + + public var backgroundColor: Color? + public var accessibilityIdentifier: String? + + public var enabled: Bool = true + public var required: Bool = true + public var readOnly: Bool = false + public var showError: Bool = false + public var inverted: Bool = false + public var surface: Surface { inverted ? .dark : .light } + + public var checkboxes: [CheckboxLabelModel] + + public var children: [any MoleculeModelProtocol] { checkboxes } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case id + case moleculeName + case accessibilityIdentifier + case inverted + case enabled + case readOnly + case showError + case checkboxes + } + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(with checkboxes: [CheckboxLabelModel]){ + self.checkboxes = checkboxes + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) + showError = try typeContainer.decodeIfPresent(Bool.self, forKey: .showError) ?? false + enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true + readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false + + if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) { + self.inverted = inverted + } + checkboxes = try typeContainer.decode([CheckboxLabelModel].self, forKey: .checkboxes) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encodeIfPresent(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) + try container.encode(readOnly, forKey: .readOnly) + try container.encode(enabled, forKey: .enabled) + try container.encode(inverted, forKey: .inverted) + try container.encode(checkboxes, forKey: .checkboxes) + } + + open func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return moleculeName == model.moleculeName + && enabled == model.enabled + && showError == model.showError + && readOnly == model.readOnly + && required == model.required + && inverted == model.inverted + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && accessibilityTraits == model.accessibilityTraits + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift index cd7623c2..5095a6ab 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift @@ -8,47 +8,33 @@ import MVMCore import VDS -@objcMembers public class RadioBoxModel: MoleculeModelProtocol, EnableableModelProtocol { +public class RadioBoxModel: FormFieldModel { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public static var identifier: String = "radioBox" - public var id: String = UUID().uuidString + public override static var identifier: String { "radioBox" } public var text: String public var subText: String? public var subTextRight: String? - public var backgroundColor: Color? - public var accessibilityIdentifier: String? public var selected: Bool = false - public var enabled: Bool = true - public var readOnly: Bool = false public var strikethrough: Bool = false - public var fieldValue: String? public var action: ActionModelProtocol? - public var inverted: Bool = false - public var surface: Surface { inverted ? .dark : .light } - + public var fieldValue: String? + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { - case id - case moleculeName case text case subText case subTextRight - case backgroundColor - case accessibilityIdentifier case selected - case enabled case strikethrough - case fieldValue case action - case readOnly - case inverted + case fieldValue } //-------------------------------------------------- @@ -57,8 +43,19 @@ import VDS public init(text: String) { self.text = text + super.init() } + //-------------------------------------------------- + // MARK: - Form Validation + //-------------------------------------------------- + + /// Returns the fieldValue of the selected box, otherwise the text of the selected box. + public override func formFieldValue() -> AnyHashable? { + guard enabled else { return nil } + return fieldValue + } + //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- @@ -66,45 +63,42 @@ import VDS required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString text = try typeContainer.decode(String.self, forKey: .text) subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText) subTextRight = try typeContainer.decodeIfPresent(String.self, forKey: .subTextRight) - accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) if let isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) { selected = isSelected } - enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true - readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false - if let isStrikeTrough = try typeContainer.decodeIfPresent(Bool.self, forKey: .strikethrough) { strikethrough = isStrikeTrough } - - if let inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) { - self.inverted = inverted - } - fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue) action = try typeContainer.decodeModelIfPresent(codingKey: .action) + try super.init(from: decoder) } - public func encode(to encoder: Encoder) throws { + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(moleculeName, forKey: .moleculeName) try container.encode(text, forKey: .text) try container.encodeIfPresent(subText, forKey: .subText) try container.encodeIfPresent(subTextRight, forKey: .subTextRight) - try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(selected, forKey: .selected) - try container.encode(enabled, forKey: .enabled) - try container.encode(readOnly, forKey: .readOnly) try container.encode(strikethrough, forKey: .strikethrough) - try container.encodeIfPresent(fieldValue, forKey: .fieldValue) try container.encodeModelIfPresent(action, forKey: .action) - try container.encode(inverted, forKey: .inverted) + } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return text == model.text + && subText == model.subText + && subTextRight == model.subTextRight + && selected == model.selected + && strikethrough == model.strikethrough + && fieldValue == model.fieldValue + && action.isEqual(to: model.action) } } + diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift index 6701f2c6..88a0fb34 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift @@ -9,10 +9,6 @@ import Foundation import VDS -public protocol RadioBoxSelectionDelegate: AnyObject { - func selected(radioBox: RadioBoxModel) -} - open class RadioBoxes: VDS.RadioBoxGroup, VDSMoleculeViewProtocol { //------------------------------------------------------ @@ -29,13 +25,12 @@ open class RadioBoxes: VDS.RadioBoxGroup, VDSMoleculeViewProtocol { /// The models for the molecules. public var boxes: [RadioBoxModel]? - public weak var radioDelegate: RadioBoxSelectionDelegate? // MARK: - MoleculeViewProtocol public func viewModelDidUpdate() { boxes = viewModel.boxes surface = viewModel.surface - selectorModels = viewModel.selectorModels + selectorModels = viewModel.boxes.convertToVDSRadioBoxModel(surface: surface) FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) } @@ -50,7 +45,8 @@ open class RadioBoxes: VDS.RadioBoxGroup, VDSMoleculeViewProtocol { boxes?.forEach { $0.selected = false } selectedBox.selected = true _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) - radioDelegate?.selected(radioBox: selectedBox) } } } + + diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift index 463afde3..310ccc54 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift @@ -8,7 +8,8 @@ import MVMCore import VDS -@objcMembers public class RadioBoxesModel: FormFieldModel { +public class RadioBoxesModel: FormFieldModel, ParentMoleculeModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -16,20 +17,9 @@ import VDS public override static var identifier: String { "radioBoxes" } public var boxes: [RadioBoxModel] + + public var children: [any MoleculeModelProtocol] { boxes } - public var selectorModels: [VDS.RadioBoxGroup.RadioBoxItemModel] { - boxes.compactMap({ item in - var radioBox = RadioBoxGroup.RadioBoxItemModel() - radioBox.text = item.text - radioBox.subText = item.subText - radioBox.subTextRight = item.subTextRight - radioBox.surface = surface - radioBox.selected = item.selected - radioBox.strikethrough = item.strikethrough - radioBox.disabled = !(item.enabled && !item.readOnly) - return radioBox - }) - } //-------------------------------------------------- // MARK: - Form Validation //-------------------------------------------------- @@ -40,7 +30,7 @@ import VDS let selectedBox = boxes.first { (box) -> Bool in return box.selected } - return selectedBox?.fieldValue ?? selectedBox?.text + return selectedBox?.formFieldValue() ?? selectedBox?.text } //-------------------------------------------------- @@ -83,3 +73,19 @@ import VDS try container.encode(boxes, forKey: .boxes) } } + +extension Array where Element == RadioBoxModel { + internal func convertToVDSRadioBoxModel(surface: Surface) -> [RadioBoxGroup.RadioBoxItemModel] { + compactMap({ item in + var radioBox = RadioBoxGroup.RadioBoxItemModel() + radioBox.text = item.text + radioBox.subText = item.subText + radioBox.subTextRight = item.subTextRight + radioBox.surface = surface + radioBox.selected = item.selected + radioBox.strikethrough = item.strikethrough + radioBox.enabled = item.isEnabled + return radioBox + }) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift index 0ecd920b..fdb98deb 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift @@ -101,7 +101,7 @@ import VDS } isSelected = viewModel.state - isEnabled = viewModel.enabled && !viewModel.readOnly + isEnabled = viewModel.isEnabled RadioButtonSelectionHelper.setupForRadioButtonGroup(viewModel, self, delegateObject: delegateObject) } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift index 5340b079..63c1ff24 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift @@ -83,4 +83,11 @@ open class RadioButtonModel: FormFieldModel { try container.encodeIfPresent(fieldValue, forKey: .fieldValue) try container.encodeModelIfPresent(action, forKey: .action) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return state == model.state + && fieldValue == model.fieldValue + && action.isEqual(to: model.action) + } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtons.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtons.swift new file mode 100644 index 00000000..bc41f8bb --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtons.swift @@ -0,0 +1,54 @@ +// +// RadioButtons.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class RadioButtons: VDS.RadioButtonGroup, VDSMoleculeViewProtocol { + + //------------------------------------------------------ + // MARK: - Properties + //------------------------------------------------------ + open var viewModel: RadioButtonsModel! + open var delegateObject: MVMCoreUIDelegateObject? + open var additionalData: [AnyHashable : Any]? + + // Form Validation + var fieldKey: String? + var fieldValue: JSONValue? + var groupName: String? + + /// The models for the molecules. + public var radioButtons: [RadioButtonLabelModel]? + + // MARK: - MoleculeViewProtocol + public func viewModelDidUpdate() { + showError = viewModel.showError + isEnabled = viewModel.isEnabled + surface = viewModel.surface + + radioButtons = viewModel.radioButtons + selectorModels = viewModel.radioButtons.convertToVDSRadioButtonItemModel(surface: surface, + delegateObject: delegateObject, + additionalData: additionalData) + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + } + + open func updateView(_ size: CGFloat) {} + + open override func didSelect(_ selectedControl: RadioButtonItem) { + super.didSelect(selectedControl) + + // since the radiobutton has the state being tracked, we need to update the values here. + if let index = items.firstIndex(where: {$0 === selectedControl}), let selected = radioButtons?[index] { + radioButtons?.forEach { $0.radioButton.state = false } + selected.radioButton.state = true + } + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonsModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonsModel.swift new file mode 100644 index 00000000..b5de4a9a --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonsModel.swift @@ -0,0 +1,74 @@ +// +// RadioButtonsModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore +import VDS + +public class RadioButtonsModel: FormFieldModel, ParentMoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public override static var identifier: String { "radioButtons" } + + public var radioButtons: [RadioButtonLabelModel] + + public var children: [any MoleculeModelProtocol] { radioButtons } + + //-------------------------------------------------- + // MARK: - Form Validation + //-------------------------------------------------- + + /// Returns the fieldValue of the selected RadioButton. + public override func formFieldValue() -> AnyHashable? { + guard enabled else { return nil } + let selected = radioButtons.first { $0.radioButton.state } + return selected?.radioButton.formFieldValue() + } + + //-------------------------------------------------- + // MARK: - Server Value + //-------------------------------------------------- + open override func formFieldServerValue() -> AnyHashable? { + return formFieldValue() + } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case radioButtons + } + + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + + public init(with radioButtons: [RadioButtonLabelModel]){ + self.radioButtons = radioButtons + super.init() + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + radioButtons = try typeContainer.decode([RadioButtonLabelModel].self, forKey: .radioButtons) + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(radioButtons, forKey: .radioButtons) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index b229d12e..feaf3399 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -108,7 +108,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) isOn = viewModel.selected surface = viewModel.surface isAnimated = viewModel.animated - isEnabled = viewModel.enabled && !viewModel.readOnly + isEnabled = viewModel.isEnabled showText = viewModel.showText if let onText = viewModel.onText { self.onText = onText diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index c310e47c..1b3e79a0 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -7,26 +7,18 @@ // import VDS -public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { +public class ToggleModel: FormFieldModel { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public static var identifier: String = "toggle" - public var id: String = UUID().uuidString - - public var accessibilityIdentifier: String? - public var backgroundColor: Color? + public override static var identifier: String { "toggle" } public var selected: Bool = false public var animated: Bool = true - public var enabled: Bool = true - public var readOnly: Bool = false public var action: ActionModelProtocol? public var alternateAction: ActionModelProtocol? public var accessibilityText: String? - public var surface: Surface { inverted ? .dark : .light } - public var inverted: Bool = false public var showText: Bool = false public var onText: String? public var offText: String? @@ -34,61 +26,41 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { public var textWeight: VDS.Toggle.TextWeight = .regular public var textPosition: VDS.Toggle.TextPosition = .left - public var fieldKey: String? - public var groupName: String = FormValidator.defaultGroupName - public var baseValue: AnyHashable? - //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { - case id - case moleculeName case state case animated - case enabled - case readOnly case action - case backgroundColor - case accessibilityIdentifier case alternateAction case accessibilityText - case inverted case showText case onText case offText case textSize case textWeight case textPosition - - case fieldKey - case groupName } //-------------------------------------------------- // MARK: - Form Valdiation //-------------------------------------------------- - public func formFieldValue() -> AnyHashable? { + public override func formFieldValue() -> AnyHashable? { guard enabled else { return nil } return selected } - //-------------------------------------------------- - // MARK: - Server Value - //-------------------------------------------------- - open func formFieldServerValue() -> AnyHashable? { - return formFieldValue() - } - //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- public init(_ state: Bool, id: String = UUID().uuidString) { - self.selected = state + selected = state + super.init() baseValue = state self.id = id } @@ -100,8 +72,6 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString - if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { self.selected = state } @@ -112,44 +82,28 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { action = try typeContainer.decodeModelIfPresent(codingKey: .action) alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction) - accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) - baseValue = selected - fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) - if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { - self.groupName = groupName - } - enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true - readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false - - inverted = try typeContainer.decodeIfPresent(Bool.self, forKey: .inverted) ?? false showText = try typeContainer.decodeIfPresent(Bool.self, forKey: .showText) ?? false onText = try typeContainer.decodeIfPresent(String.self, forKey: .onText) offText = try typeContainer.decodeIfPresent(String.self, forKey: .offText) textSize = try typeContainer.decodeIfPresent(VDS.Toggle.TextSize.self, forKey: .textSize) ?? .small textWeight = try typeContainer.decodeIfPresent(VDS.Toggle.TextWeight.self, forKey: .textWeight) ?? .regular textPosition = try typeContainer.decodeIfPresent(VDS.Toggle.TextPosition.self, forKey: .textPosition) ?? .left + + try super.init(from: decoder) + baseValue = selected } - public func encode(to encoder: Encoder) throws { + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) - try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(alternateAction, forKey: .alternateAction) - try container.encode(moleculeName, forKey: .moleculeName) try container.encode(selected, forKey: .state) try container.encode(animated, forKey: .animated) - try container.encode(enabled, forKey: .enabled) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) - try container.encodeIfPresent(fieldKey, forKey: .fieldKey) - try container.encodeIfPresent(groupName, forKey: .groupName) - try container.encode(readOnly, forKey: .readOnly) - - try container.encode(inverted, forKey: .inverted) try container.encode(showText, forKey: .showText) try container.encodeIfPresent(onText, forKey: .onText) try container.encodeIfPresent(offText, forKey: .offText) @@ -157,4 +111,19 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { try container.encode(textWeight, forKey: .textWeight) try container.encode(textPosition, forKey: .textPosition) } + + open override func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard super.isEqual(to: model), let model = model as? Self else { return false } + return selected == model.selected + && animated == model.animated + && action.isEqual(to: model.action) + && alternateAction.isEqual(to: model.alternateAction) + && accessibilityText == model.accessibilityText + && showText == model.showText + && onText == model.onText + && offText == model.offText + && textSize == model.textSize + && textWeight == model.textWeight + && textPosition == model.textPosition + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Calendar.swift b/MVMCoreUI/Atomic/Atoms/Views/Calendar.swift new file mode 100644 index 00000000..e6e8bb28 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Calendar.swift @@ -0,0 +1,12 @@ +// +// Calendar.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class CalendarView: VDS.CalendarBase, VDSMoleculeViewProtocol diff --git a/MVMCoreUI/Atomic/Atoms/Views/CalendarView.swift b/MVMCoreUI/Atomic/Atoms/Views/CalendarView.swift new file mode 100644 index 00000000..9a0850e8 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CalendarView.swift @@ -0,0 +1,62 @@ +// +// Calendar.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class CalendarView: VDS.CalendarBase, VDSMoleculeViewProtocol { + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var viewModel: CalendarViewModel! + public var delegateObject: MVMCoreUIDelegateObject? + public var additionalData: [AnyHashable : Any]? + + //-------------------------------------------------- + // MARK: - Public Methods + //-------------------------------------------------- + + public func viewModelDidUpdate() { + if let _selectedDate = viewModel.selectedDate { + selectedDate = _selectedDate + } + + if let _activeDates = viewModel.activeDates { + activeDates = _activeDates + } + + if let _hideContainerBorder = viewModel.hideContainerBorder { + hideContainerBorder = _hideContainerBorder + } + + if let _hideCurrentDateIndicator = viewModel.hideCurrentDateIndicator { + hideCurrentDateIndicator = _hideCurrentDateIndicator + } + + if let _inactiveDates = viewModel.inactiveDates { + inactiveDates = _inactiveDates + } + + if let _indicators = viewModel.indicators { + indicators = _indicators + } + + if let _maxDate = viewModel.maxDate { + maxDate = _maxDate + } + + if let _minDate = viewModel.minDate { + minDate = _minDate + } + + surface = viewModel.surface + } + + public func updateView(_ size: CGFloat) {} + +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CalendarViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CalendarViewModel.swift new file mode 100644 index 00000000..3abb4cc5 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CalendarViewModel.swift @@ -0,0 +1,153 @@ +// +// CalendarModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 8/20/24. +// Copyright © 2024 Verizon Wireless. All rights reserved. +// + +import Foundation +import VDS + +open class CalendarViewModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "calendar" + public var id: String = UUID().uuidString + public var backgroundColor: Color? + + public var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeZone = NSTimeZone.system + formatter.locale = .current + formatter.formatterBehavior = .default + return formatter + }() + + /// Update the property value to alter the format of how the date is presented. + public var dateFormat: String = "MMM d, y" { + didSet { dateFormatter.dateFormat = dateFormat } + } + + public var hideContainerBorder: Bool? + public var hideCurrentDateIndicator: Bool? + public var activeDates: [Date]? + public var inactiveDates: [Date]? + public var selectedDate: Date? + public var minDate: Date? + public var maxDate: Date? + public var indicators: [CalendarBase.CalendarIndicatorModel]? + + //-------------------------------------------------- + // MARK: - VDS Properties + //-------------------------------------------------- + public var surface: Surface { inverted ? .dark : .light } + public var inverted: Bool = false + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { + case id + case inverted + case dateFormat + case hideContainerBorder + case hideCurrentDateIndicator + case activeDates + case inactiveDates + case selectedDate + case minDate + case maxDate + case indicators + + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + public init() {} + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + inverted = try container.decodeIfPresent(Bool.self, forKey: .inverted) ?? false + + hideContainerBorder = try container.decodeIfPresent(Bool.self, forKey: .hideContainerBorder) + hideCurrentDateIndicator = try container.decodeIfPresent(Bool.self, forKey: .hideCurrentDateIndicator) + + if let dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) { + self.dateFormat = dateFormat + dateFormatter.dateFormat = dateFormat + } + + if let dates = try container.decodeIfPresent([String].self, forKey: .activeDates) { + activeDates = dates.compactMap { dateFormatter.date(from: $0) } + } + + if let dates = try container.decodeIfPresent([String].self, forKey: .inactiveDates) { + inactiveDates = dates.compactMap { dateFormatter.date(from: $0) } + } + + if let date = try container.decodeIfPresent(String.self, forKey: .selectedDate) { + selectedDate = dateFormatter.date(from: date) + } + + if let date = try container.decodeIfPresent(String.self, forKey: .minDate) { + minDate = dateFormatter.date(from: date) + } + + if let date = try container.decodeIfPresent(String.self, forKey: .maxDate) { + maxDate = dateFormatter.date(from: date) + } + + indicators = try container.decodeIfPresent([CalendarBase.CalendarIndicatorModel].self, forKey: .indicators) + + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(inverted, forKey: .inverted) + try container.encode(dateFormat, forKey: .dateFormat) + try container.encode(hideContainerBorder, forKey: .hideContainerBorder) + try container.encode(hideCurrentDateIndicator, forKey: .hideCurrentDateIndicator) + try container.encode(activeDates, forKey: .activeDates) + try container.encode(selectedDate, forKey: .selectedDate) + try container.encode(minDate, forKey: .minDate) + try container.encode(maxDate, forKey: .maxDate) + try container.encode(indicators, forKey: .indicators) + } + + open func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return inverted == model.inverted + && dateFormat == model.dateFormat + && hideContainerBorder == model.hideContainerBorder + && hideCurrentDateIndicator == model.hideCurrentDateIndicator + && activeDates == model.activeDates + && inactiveDates == model.inactiveDates + && selectedDate == model.selectedDate + && minDate == model.minDate + && maxDate == model.maxDate + && indicators == model.indicators + } +} + +extension CalendarViewModel { + public func convertToVDSCalendarModel() -> DatePicker.CalendarModel { + let defaults = DatePicker.CalendarModel() + return .init(hideContainerBorder: hideContainerBorder ?? defaults.hideContainerBorder , + hideCurrentDateIndicator: hideCurrentDateIndicator ?? defaults.hideCurrentDateIndicator, + activeDates: activeDates ?? defaults.activeDates, + inactiveDates: inactiveDates ?? defaults.inactiveDates, + selectedDate: selectedDate ?? defaults.selectedDate, + minDate: minDate ?? defaults.minDate, + maxDate: maxDate ?? defaults.maxDate, + indicators: indicators ?? defaults.indicators) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabel.swift b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabel.swift index b4825ee6..919cd953 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabel.swift @@ -64,7 +64,7 @@ import VDS //properties isAnimated = viewModel.checkbox.animated - isEnabled = viewModel.checkbox.enabled && !viewModel.checkbox.readOnly + isEnabled = viewModel.checkbox.isEnabled //call super here to go around the didSet //in this class @@ -78,7 +78,7 @@ import VDS //TODO: Fix issue with default state //showError = !isValid errorText = viewModel.checkbox.errorMessage - isEnabled = viewModel.checkbox.enabled + isEnabled = viewModel.checkbox.isEnabled }) } diff --git a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift index faa13e60..fde30ae6 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift @@ -36,5 +36,36 @@ import VDS self.label = label self.subTitle = subTitle } - + + open func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return moleculeName == model.moleculeName + && inverted == model.inverted + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && accessibilityTraits == model.accessibilityTraits + } +} + +extension Array where Element == CheckboxLabelModel { + internal func convertToVDSCheckboxItemModel(surface: Surface, + delegateObject: MVMCoreUIDelegateObject?, + additionalData: [AnyHashable: Any]?) -> [CheckboxGroup.CheckboxItemModel] { + return compactMap({ model in + var item = CheckboxGroup.CheckboxItemModel() + item.inputId = model.checkbox.fieldKey + item.labelText = model.label.text + if let attributes = model.label.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) { + item.labelTextAttributes = attributes + } + item.childText = model.subTitle?.text + if let attributes = model.subTitle?.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) { + item.childTextAttributes = attributes + } + item.surface = surface + item.selected = model.checkbox.selected + item.enabled = model.checkbox.isEnabled + return item + }) + } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift index 974cca8c..0c9886d7 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TooltipModel.swift @@ -98,7 +98,7 @@ open class TooltipModel: MoleculeModelProtocol { } extension TooltipModel { - public func toVDSTooltipModel() -> Tooltip.TooltipModel { + public func convertToVDSTooltipModel() -> Tooltip.TooltipModel { var moleculeView: MoleculeViewProtocol? if let molecule, let view = ModelRegistry.createMolecule(molecule) { moleculeView = view diff --git a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift index 8f565216..6c6ae71f 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-Enums+Codable.swift @@ -9,6 +9,7 @@ import Foundation import VDS import VDSCoreTokens +import MVMCore //-------------------------------------------------- // MARK: - Codable Extensions @@ -57,6 +58,52 @@ extension VDS.Button.Size: RawRepresentableCodable { public static var defaultValue: VDS.Button.Size? { nil } } +extension VDS.CalendarBase.CalendarIndicatorModel: Codable, ModelComparisonProtocol, MoleculeModelComparisonProtocol { + + enum CodingKeys: String, CodingKey { + case label + case date + case dateFormat + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let label = try container.decode(String.self, forKey: .label) + let formatter = Self.formatter() + formatter.dateFormat = try container.decodeIfPresent(String.self, forKey: .dateFormat) ?? "MMM d, y" + let foundDate = try container.decode(String.self, forKey: .date) + let date = formatter.date(from: foundDate)! + self = .init(label: label, date: date) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(label, forKey: .label) + try container.encode(Self.formatter().string(from: date), forKey: .date) + } + + static func formatter() -> DateFormatter { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeZone = NSTimeZone.system + formatter.locale = .current + formatter.formatterBehavior = .default + return formatter + } + + public func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return label == model.label + && date == model.date + } + + public func isVisuallyEquivalent(to model: any MoleculeModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return label == model.label + && date == model.date + } +} + //-------------------------------------------------- // MARK: - Decodable Defaults //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Extensions/VDS-LabelAttributeModel.swift b/MVMCoreUI/Atomic/Extensions/VDS-LabelAttributeModel.swift index 4eab03b2..6b31175c 100644 --- a/MVMCoreUI/Atomic/Extensions/VDS-LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Extensions/VDS-LabelAttributeModel.swift @@ -16,7 +16,7 @@ extension Array where Element: MVMCoreUI.LabelAttributeModel { var attributes: [any VDS.LabelAttributeModel] = [] forEach { atomicLabelAttribute in if let attr = atomicLabelAttribute as? (any VDSLabelAttributeConvertable), - let vds = attr.convertToVDSLabelAttirbute(delegateObject: delegateObject, + let vds = attr.convertToVDSLabelAttribute(delegateObject: delegateObject, additionalData: additionalData){ attributes.append(vds) } @@ -28,12 +28,12 @@ extension Array where Element: MVMCoreUI.LabelAttributeModel { //VDS Convertable Protocol and Extensions public protocol VDSLabelAttributeConvertable { associatedtype LabelAttributeType: VDS.LabelAttributeModel - func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> LabelAttributeType? + func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> LabelAttributeType? } extension LabelAttributeUnderlineModel: VDSLabelAttributeConvertable { - public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> UnderlineLabelAttribute? { + public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> UnderlineLabelAttribute? { guard let style = UnderlineLabelAttribute.Style(rawValue: style.rawValue) else { return nil } var pattern: UnderlineLabelAttribute.Pattern? @@ -50,7 +50,7 @@ extension LabelAttributeUnderlineModel: VDSLabelAttributeConvertable { } extension LabelAttributeActionModel: VDSLabelAttributeConvertable { - public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ActionLabelAttribute? { + public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ActionLabelAttribute? { var vdsAttribute = VDS.ActionLabelAttribute(location: location, length: length) vdsAttribute.subscriber = vdsAttribute.action.sink { [weak self] in guard let self else { return } @@ -64,7 +64,7 @@ extension LabelAttributeActionModel: VDSLabelAttributeConvertable { } extension LabelAttributeFontModel: VDSLabelAttributeConvertable { - public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> TextStyleLabelAttribute? { + public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> TextStyleLabelAttribute? { var textStyle: TextStyle? if let found = style?.vdsTextStyle() { @@ -82,7 +82,7 @@ extension LabelAttributeFontModel: VDSLabelAttributeConvertable { } extension LabelAttributeColorModel: VDSLabelAttributeConvertable { - public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ColorLabelAttribute? { + public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> ColorLabelAttribute? { guard let textColor else { return nil } return ColorLabelAttribute(location: location, length: length, @@ -91,14 +91,14 @@ extension LabelAttributeColorModel: VDSLabelAttributeConvertable { } extension LabelAttributeStrikeThroughModel: VDSLabelAttributeConvertable { - public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> StrikeThroughLabelAttribute? { + public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> StrikeThroughLabelAttribute? { return StrikeThroughLabelAttribute(location: location, length: length) } } extension LabelAttributeImageModel: VDSLabelAttributeConvertable { - public func convertToVDSLabelAttirbute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> AtomicImageLabelAttribute? { + public func convertToVDSLabelAttribute(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> AtomicImageLabelAttribute? { var frame: CGRect? if let size { frame = CGRect(x: 0, y: 0, width: size, height: size) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabel.swift index 39a578ba..5d2c10bb 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabel.swift @@ -106,7 +106,7 @@ import VDS } //properties - isEnabled = viewModel.radioButton.enabled && !viewModel.radioButton.readOnly + isEnabled = viewModel.radioButton.isEnabled isSelected = viewModel.radioButton.state //forms diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift index 0f4a4be6..a5863694 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift @@ -40,5 +40,36 @@ import VDS self.label = label self.subTitle = subTitle } - + + open func isEqual(to model: any ModelComparisonProtocol) -> Bool { + guard let model = model as? Self else { return false } + return moleculeName == model.moleculeName + && inverted == model.inverted + && accessibilityText == model.accessibilityText + && accessibilityIdentifier == model.accessibilityIdentifier + && accessibilityTraits == model.accessibilityTraits + } +} + +extension Array where Element == RadioButtonLabelModel { + internal func convertToVDSRadioButtonItemModel(surface: Surface, + delegateObject: MVMCoreUIDelegateObject?, + additionalData: [AnyHashable: Any]?) -> [RadioButtonGroup.RadioButtonItemModel] { + return compactMap({ model in + var item = RadioButtonGroup.RadioButtonItemModel() + item.inputId = model.radioButton.fieldKey + item.labelText = model.label.text + if let attributes = model.label.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) { + item.labelTextAttributes = attributes + } + item.childText = model.subTitle?.text + if let attributes = model.subTitle?.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) { + item.childTextAttributes = attributes + } + item.surface = surface + item.selected = model.radioButton.state + item.enabled = model.radioButton.isEnabled + return item + }) + } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift index 97014092..5628faba 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeComparisonProtocol.swift @@ -70,3 +70,43 @@ public extension Optional where Wrapped: Collection { return self.isVisuallyEquivalent(to: models) } } + +func == (lhs: (any MoleculeModelComparisonProtocol)?, rhs: (any MoleculeModelComparisonProtocol)?) -> Bool { + switch (lhs, rhs) { + case (.some(let lhs), .some(let rhs)): + return lhs.isEqual(to: rhs) + case (.none, .none): + return true + default: + return false + } +} + +func != (lhs: (any MoleculeModelComparisonProtocol)?, rhs: (any MoleculeModelComparisonProtocol)?) -> Bool { + return !(lhs == rhs) +} + +func == (lhs: [any MoleculeModelComparisonProtocol]?, rhs: [any MoleculeModelComparisonProtocol]?) -> Bool { + switch (lhs, rhs) { + case (.some(let lhs), .some(let rhs)): + return lhs == rhs + case (.none, .none): + return true + default: + return false + } +} + +func != (lhs: [any MoleculeModelComparisonProtocol]?, rhs: [any MoleculeModelComparisonProtocol]?) -> Bool { + return !(lhs == rhs) +} + +func == (lhs: [any MoleculeModelComparisonProtocol], rhs: [any MoleculeModelComparisonProtocol]) -> Bool { + return lhs.elementsEqual(rhs, by: { (lhsElement, rhsElement) -> Bool in + return lhsElement == rhsElement + }) +} + +func != (lhs: [any MoleculeModelComparisonProtocol], rhs: [any MoleculeModelComparisonProtocol]) -> Bool { + return !(lhs == rhs) +} diff --git a/MVMCoreUI/Atomic/Protocols/VDSMoleculeViewProtocol.swift b/MVMCoreUI/Atomic/Protocols/VDSMoleculeViewProtocol.swift index 2bb95c43..8dfb9ae3 100644 --- a/MVMCoreUI/Atomic/Protocols/VDSMoleculeViewProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/VDSMoleculeViewProtocol.swift @@ -14,10 +14,12 @@ import MVMCore ///----------------------------------------------------------------------------- public protocol VDSMoleculeViewProtocol: MoleculeViewProtocol, MVMCoreViewProtocol { associatedtype ViewModel: MoleculeModelProtocol + var shouldUpdateView: Bool { get set } var viewModel: ViewModel! { get set } var delegateObject: MVMCoreUIDelegateObject? { get set } var additionalData: [AnyHashable: Any]? { get set } func viewModelDidUpdate() + func setNeedsUpdate() } extension VDSMoleculeViewProtocol { @@ -32,7 +34,10 @@ extension VDSMoleculeViewProtocol { self.delegateObject = delegateObject self.additionalData = additionalData viewModel = castedModel + shouldUpdateView = false viewModelDidUpdate() + shouldUpdateView = true + setNeedsUpdate() } public func update(viewModel: ViewModel){ diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index c45936cf..68494d90 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -41,6 +41,7 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self) // MARK:- Entry Field + ModelRegistry.register(handler: DatePickerEntryField.self, for: DatePickerEntryFieldModel.self) ModelRegistry.register(handler: InputEntryField.self, for: TextEntryFieldModel.self) ModelRegistry.register(handler: MdnEntryField.self, for: MdnEntryFieldModel.self) ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self) @@ -50,8 +51,10 @@ open class CoreUIModelMapping: ModelMapping { // MARK:- Selectors ModelRegistry.register(handler: RadioButton.self, for: RadioButtonModel.self) + ModelRegistry.register(handler: RadioButtons.self, for: RadioButtonsModel.self) ModelRegistry.register(handler: RadioBoxes.self, for: RadioBoxesModel.self) ModelRegistry.register(handler: Checkbox.self, for: CheckboxModel.self) + ModelRegistry.register(handler: Checkboxes.self, for: CheckboxesModel.self) ModelRegistry.register(handler: RadioSwatches.self, for: RadioSwatchesModel.self) ModelRegistry.register(handler: Tags.self, for: TagsModel.self) ModelRegistry.register(handler: Tag.self, for: TagModel.self) @@ -60,6 +63,7 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: Star.self, for: StarModel.self) // MARK:- Other Atoms + ModelRegistry.register(handler: CalendarView.self, for: CalendarViewModel.self) ModelRegistry.register(handler: ProgressBar.self, for: ProgressBarModel.self) ModelRegistry.register(handler: MultiProgress.self, for: MultiProgressBarModel.self) ModelRegistry.register(handler: CaretView.self, for: CaretViewModel.self)