diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index e8c458bd..fbb120aa 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -598,6 +598,10 @@ 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 */; }; @@ -1231,6 +1235,10 @@ 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 = ""; }; @@ -2374,6 +2382,8 @@ EA7D815F2B2B6E6800D29F9E /* Icon.swift */, EA7D81632B2BABCB00D29F9E /* TooltipModel.swift */, EA7D81652B2BABD200D29F9E /* Tooltip.swift */, + EA7AE5502C74EB4500107C74 /* CalendarViewModel.swift */, + EA7AE54E2C74EB3700107C74 /* CalendarView.swift */, ); path = Views; sourceTree = ""; @@ -2525,6 +2535,8 @@ D2BEFEF5248A954C00FAB3A9 /* FormFields */ = { isa = PBXGroup; children = ( + EA7AE5542C74F20600107C74 /* DatePickerEntryFieldModel.swift */, + EA7AE5522C74F1F600107C74 /* DatePickerEntryField.swift */, EA5DBDAA2C35B6C500290DF8 /* FormFieldModel.swift */, D2BEFEF6248A957A00FAB3A9 /* Tags */, D29DF22B21E6A0FA003B2FB9 /* TextFields */, @@ -2875,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 */, @@ -3123,6 +3136,7 @@ 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 */, @@ -3310,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 */, @@ -3371,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..b798b068 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryField.swift @@ -0,0 +1,44 @@ +// +// 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 + //-------------------------------------------------- + + public func viewModelDidUpdate() { + surface = viewModel.surface + labelText = "Date" + helperText = "Pick a date" + selectedDate = viewModel.selectedDate + calendarModel = viewModel.calendar.convertToVDSCalendarModel() + FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate) + } + + open override func setDefaults() { + super.setDefaults() + onChange = { [weak self] control in + guard let self, let viewModel, isEnabled else { return } + viewModel.selectedDate = control.selectedDate + _ = FormValidator.validate(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..68b63309 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/DatePickerEntryFieldModel.swift @@ -0,0 +1,82 @@ +// +// 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() + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { + case dateFormat + case selectedDate + case calendar + } + + //-------------------------------------------------- + // MARK: - Form Validation + //-------------------------------------------------- + + /// Returns the fieldValue of the selected box, otherwise the text of the selected box. + 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 + } + + 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) + } +} 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..16e9b425 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/CalendarViewModel.swift @@ -0,0 +1,131 @@ +// +// 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) + } +} + +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) + } +}