diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 1d2c67eb..e2235ca3 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -571,6 +571,13 @@ DBEFFA04225A829700230692 /* Label.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB891E822253FA8500022516 /* Label.swift */; }; EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; }; EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; }; + EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */; }; + EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */; }; + EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */; }; + EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */; }; + EAA0CFB3275E831E00D65EB0 /* DisableFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */; }; + EABFC1412763BB8D00E78B40 /* StateLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFC1402763BB8D00E78B40 /* StateLabel.swift */; }; + EABFC152276913E800E78B40 /* StateLabelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFC151276913E800E78B40 /* StateLabelModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -1141,6 +1148,13 @@ DBC4391A224421A0001AB423 /* CaretLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaretLink.swift; sourceTree = ""; }; EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = ""; }; EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = ""; }; + EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnableFormFieldEffectModel.swift; sourceTree = ""; }; + EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIUpdatableModelProtocol.swift; sourceTree = ""; }; + EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormFieldEffectProtocol.swift; sourceTree = ""; }; + EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HideFormFieldEffectModel.swift; sourceTree = ""; }; + EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableFormFieldEffectModel.swift; sourceTree = ""; }; + EABFC1402763BB8D00E78B40 /* StateLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateLabel.swift; sourceTree = ""; }; + EABFC151276913E800E78B40 /* StateLabelModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateLabelModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1206,6 +1220,7 @@ 01C74D87224298E2009C25A3 /* FormUIHelpers */ = { isa = PBXGroup; children = ( + EAA0CFAD275E7D5A00D65EB0 /* FormFieldEffect */, 011D95882404249B000E3791 /* FormHolderModelProtocol.swift */, 011D95AC2406BB57000E3791 /* FormHolderProtocol.swift */, 011D95AA2405C553000E3791 /* FormItemProtocol.swift */, @@ -1213,6 +1228,7 @@ 011D95A824057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift */, 011D9601240DA20A000E3791 /* FormRuleWatcherFieldProtocol.swift */, 0105618A224BBE7700E1557D /* FormValidator.swift */, + EA7E67752758365300ABF773 /* UIUpdatableModelProtocol.swift */, 011D958A24042794000E3791 /* Rules */, ); path = FormUIHelpers; @@ -1428,6 +1444,8 @@ 94C2D9A823872E5E0006CF46 /* LabelAttributeImageModel.swift */, 94C2D9AA23872EB50006CF46 /* LabelAttributeActionModel.swift */, DB891E822253FA8500022516 /* Label.swift */, + EABFC151276913E800E78B40 /* StateLabelModel.swift */, + EABFC1402763BB8D00E78B40 /* StateLabel.swift */, ); path = Label; sourceTree = ""; @@ -2400,6 +2418,17 @@ path = Alerts; sourceTree = ""; }; + EAA0CFAD275E7D5A00D65EB0 /* FormFieldEffect */ = { + isa = PBXGroup; + children = ( + EAA0CFAE275E7D8000D65EB0 /* FormFieldEffectProtocol.swift */, + EA7E67732758310500ABF773 /* EnableFormFieldEffectModel.swift */, + EAA0CFB2275E831E00D65EB0 /* DisableFormFieldEffectModel.swift */, + EAA0CFB0275E823A00D65EB0 /* HideFormFieldEffectModel.swift */, + ); + path = FormFieldEffect; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2538,6 +2567,7 @@ 324FB6AA249366F3002552C7 /* ListLeftVariableNumberedListBodyTextModel.swift in Sources */, 5248BFED23F12E350059236A /* ListThreeColumnPlanDataDividerModel.swift in Sources */, AA0A257824766C8A00862F64 /* ListLeftVariableIconWithRightCaretBodyTextModel.swift in Sources */, + EABFC152276913E800E78B40 /* StateLabelModel.swift in Sources */, 0A5D59C223AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift in Sources */, 0AF60F0926B3316E00AC3DB4 /* MVMCoreUIUtility+Extension.swift in Sources */, 8D070BB0241B56530099AC56 /* ListRightVariableTotalDataModel.swift in Sources */, @@ -2606,6 +2636,7 @@ D20C700B250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift in Sources */, D236E5B4241FEB1000C38625 /* ListTwoColumnPriceDescription.swift in Sources */, 0AA33B3A2398524F0067DD0F /* Toggle.swift in Sources */, + EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */, D29DF12F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m in Sources */, 942C378C2412F4FA0066E45E /* ModalMoleculeListTemplate.swift in Sources */, BB47A588241615FA002BB23C /* ListOneColumnFullWidthTextDividerSubsection.swift in Sources */, @@ -2748,6 +2779,7 @@ D2CAC7CD251104FE00C75681 /* NotificationModel.swift in Sources */, D2ED2803254B0E0300A1C293 /* MVMCoreAlertHandler.m in Sources */, 0A1B4A96233BB18F005B3FB4 /* CheckboxLabel.swift in Sources */, + EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */, D20923592450ECE00044AD09 /* TableView.swift in Sources */, BB47A586241615EF002BB23C /* ListOneColumnFullWidthTextDividerSubsectionModel.swift in Sources */, AA69AAF82445BF6800AF3D3B /* ListLeftVariableCheckboxBodyTextModel.swift in Sources */, @@ -2959,6 +2991,7 @@ 013F801923FB4A8E00AD8013 /* UIContentMode+Extension.swift in Sources */, AA104AC724472DB0004D2810 /* HeadersH1Button.swift in Sources */, 525239C22407BD1000454969 /* ListTwoColumnPriceDetails.swift in Sources */, + EABFC1412763BB8D00E78B40 /* StateLabel.swift in Sources */, AA997252247530B100FC7472 /* ListLeftVariableIconAllTextLinks.swift in Sources */, D2A5146122121FBF00345BFB /* MoleculeStackTemplate.swift in Sources */, D23EA7FB2475F09800D60C34 /* CarouselItemProtocol.swift in Sources */, @@ -2968,12 +3001,14 @@ 27F973532466074500CAB5C5 /* PageBehaviorProtocol.swift in Sources */, 94C2D9A323872C110006CF46 /* LabelAttributeStrikeThroughModel.swift in Sources */, D28A838523CCCA8900DFE4FC /* ScrollerModel.swift in Sources */, + EAA0CFB3275E831E00D65EB0 /* DisableFormFieldEffectModel.swift in Sources */, D29DF26C21E6AA0B003B2FB9 /* FLAnimatedImage.m in Sources */, D23EA7FE247EBBB700D60C34 /* NavigationLabelButtonModel.swift in Sources */, D28A839123CD4FD400DFE4FC /* CornerLabelsModel.swift in Sources */, 012A88F123985E0100FE3DA1 /* Color.swift in Sources */, D22D8393241C27B100D3DF69 /* TemplateModel.swift in Sources */, 012A889C23889E8400FE3DA1 /* TemplateModelProtocol.swift in Sources */, + EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */, D23A8FFB26123189007E14CE /* PageBehaviorModelProtocol.swift in Sources */, 52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */, D2ED2812254B0EB800A1C293 /* MVMCoreTopAlertObject.m in Sources */, @@ -3040,6 +3075,7 @@ 011D95AD2406BB57000E3791 /* FormHolderProtocol.swift in Sources */, D23A9004261234CE007E14CE /* PageBehaviorHandlerProtocol.swift in Sources */, 01509D932327ECFB00EF99AA /* ProgressBar.swift in Sources */, + EA7E67762758365300ABF773 /* UIUpdatableModelProtocol.swift in Sources */, D2169301251E51E7002A6324 /* SectionListTemplate.swift in Sources */, 0A6682AA2435125F00AD3CA1 /* Styler.swift in Sources */, D264FAA7243FE13B00D98315 /* RadioBox.swift in Sources */, diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 9814a44a..a8e004c2 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -11,7 +11,7 @@ import UIKit public typealias FacadeElements = (fill: UIColor?, text: UIColor?, border: UIColor?) -open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol, EnableableModelProtocol { +open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -59,12 +59,6 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- - - public func setValidity(_ valid: Bool, group: FormGroupRule) { - enabled = valid - updateUI?() - } - /// Temporary binding mechanism for the view to update on enable changes. public var updateUI: ActionBlock? diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift index d8217d0d..560027f3 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift @@ -8,7 +8,7 @@ import Foundation -open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol, EnableableModelProtocol { +open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWatcherFieldProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -25,11 +25,6 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro public var disabledTintColor: Color? public var groupName: String = "" - - public func setValidity(_ valid: Bool, group: FormGroupRule) { - enabled = valid - updateUI?() - } public var updateUI: ActionBlock? 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 bf7416e6..02fa342e 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 @@ -21,6 +21,7 @@ //-------------------------------------------------- public override func formFieldValue() -> AnyHashable? { + guard enabled else { return nil } guard !options.isEmpty, let index = selectedIndex else { return nil } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift index 8799f6aa..42dbf463 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift @@ -25,7 +25,7 @@ import Foundation //-------------------------------------------------- public override func formFieldValue() -> AnyHashable? { - + guard enabled else { return nil } guard !components.isEmpty && !selectedIndexes.isEmpty else { return nil } return selectedRowText diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift index db6ffe53..ca8a7156 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryField.swift @@ -17,11 +17,8 @@ import UIKit //-------------------------------------------------- // MARK: - Outlets //-------------------------------------------------- - - public private(set) var titleLabel: Label = { - let label = Label() - label.font = Styler.Font.RegularMicro.getFont() - label.textColor = .mvmBlack + public private(set) var titleLabel: StateLabel = { + let label = StateLabel() label.setContentCompressionResistancePriority(.required, for: .vertical) return label }() @@ -29,10 +26,8 @@ import UIKit public private(set) var entryFieldContainer = EntryFieldContainer() /// Provides contextual information on the TextField. - public private(set) var feedbackLabel: Label = { - let label = Label() - label.font = Styler.Font.RegularMicro.getFont() - label.textColor = .mvmBlack + public private(set) var feedbackLabel: StateLabel = { + let label = StateLabel() label.setContentCompressionResistancePriority(.required, for: .vertical) return label }() @@ -46,7 +41,6 @@ import UIKit //-------------------------------------------------- // MARK: - Stored Properties //-------------------------------------------------- - public var isValid: Bool = false /// Validate on each entry in the textField. Default: true @@ -60,10 +54,15 @@ import UIKit public var isEnabled: Bool { get { entryFieldContainer.isEnabled } set (enabled) { - titleLabel.textColor = enabled ? .mvmBlack : .mvmCoolGray3 - feedbackLabel.textColor = enabled ? .mvmBlack : .mvmCoolGray3 - entryFieldContainer.isEnabled = enabled - entryFieldModel?.enabled = enabled + if(entryFieldContainer.isEnabled != enabled){ + titleLabel.isEnabled = enabled + feedbackLabel.isEnabled = enabled + entryFieldContainer.isEnabled = enabled + entryFieldModel?.enabled = enabled + if !enabled { + self.text = defaultText + } + } } } @@ -71,8 +70,11 @@ import UIKit public var showError: Bool { get { entryFieldContainer.showError } set (error) { - feedback = error ? errorMessage : entryFieldModel?.feedback - feedbackLabel.textColor = error ? entryFieldModel?.errorTextColor?.uiColor ?? .mvmBlack : .mvmBlack + if error { + feedbackLabel.showError(message: self.errorMessage ?? "") + } else { + feedbackLabel.reset() + } entryFieldContainer.showError = error entryFieldModel?.showError = error } @@ -104,11 +106,11 @@ import UIKit public var title: String? { get { titleLabel.text } set { - titleLabel.text = newValue + titleLabel.set(text: newValue ?? "") setAccessibilityString(newValue) } } - + /// Override this to conveniently get/set the textfield(s). public var text: String? { get { nil } @@ -119,7 +121,7 @@ import UIKit public var feedback: String? { get { feedbackLabel.text } set { - feedbackLabel.text = newValue + feedbackLabel.set(text: newValue ?? "") feedbackLabel.accessibilityElementsHidden = feedbackLabel.text?.isEmpty ?? true } } @@ -128,6 +130,9 @@ import UIKit model as? EntryFieldModel } + ///This is the value of the entryFieldModel.text on set + ///This is used to update the text value on a reset or when enabled is set to false + private var defaultText: String = "" //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- @@ -299,10 +304,17 @@ import UIKit super.set(with: model, delegateObject, additionalData) self.delegateObject = delegateObject - guard let model = model as? EntryFieldModel else { return } + guard let model = entryFieldModel else { return } entryFieldContainer.set(with: model, delegateObject, additionalData) + //setup the properties for setting models + //later with enabled/showError + titleLabel.setup(model: model.titleStateLabel, delegateObject, additionalData) + feedbackLabel.setup(model: model.feedbackStateLabel, delegateObject, additionalData) + + isEnabled = model.enabled + defaultText = model.text ?? "" model.updateUI = { [weak self] in MVMCoreDispatchUtility.performBlock(onMainThread: { guard let self = self else { return } @@ -313,6 +325,7 @@ import UIKit } else if model.isValid ?? true && self.showError { self.showError = false } + self.isEnabled = model.enabled }) } @@ -328,13 +341,10 @@ import UIKit self.updateValidation(validState) }) } - - title = model.title - feedback = model.feedback - isEnabled = model.enabled + entryFieldContainer.disableAllBorders = model.hideBorders accessibilityIdentifier = model.accessibilityIdentifier ?? model.fieldKey - + if let isLocked = model.locked { self.isLocked = isLocked @@ -364,3 +374,11 @@ extension EntryField { // To Be Overriden } } + +extension LabelModel { + public convenience init(fontStyle: Styler.Font, textColor: Color) { + self.init(text: "") + self.fontStyle = fontStyle + self.textColor = textColor + } +} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift index 2a2e737c..9520406f 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift @@ -9,7 +9,8 @@ import Foundation -@objcMembers open class EntryFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, EnableableModelProtocol { +@objcMembers open class EntryFieldModel: MoleculeModelProtocol, FormFieldProtocol, FormRuleWatcherFieldProtocol, UIUpdatableModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,8 +19,6 @@ import Foundation public var backgroundColor: Color? public var accessibilityIdentifier: String? - public var title: String? - public var feedback: String? public var shouldClearText: Bool = false public var dynamicErrorMessage: String? { didSet { @@ -40,6 +39,20 @@ import Foundation public var baseValue: AnyHashable? public var wasInitiallySelected: Bool = false + //text only + //Used for re-encoding what was decoded + private var title: String? + private var feedback: String? + + //label models + //Used for re-encoding what was decoded + private var titleLabel: LabelModel? + private var feedbackLabel: LabelModel? + + //used to drive the EntryFieldView UI + public var titleStateLabel: StateLabelModel + public var feedbackStateLabel: StateLabelModel + public var isValid: Bool? = true { didSet { updateUI?() } } @@ -70,6 +83,8 @@ import Foundation case text case fieldKey case groupName + case titleLabel + case feedbackLabel } //-------------------------------------------------- @@ -77,6 +92,8 @@ import Foundation //-------------------------------------------------- public func formFieldValue() -> AnyHashable? { + guard enabled else { return nil } + if dynamicErrorMessage != nil { dynamicErrorMessage = nil } @@ -92,7 +109,7 @@ import Foundation self.isValid = valid updateUI?() } - + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -100,6 +117,8 @@ import Foundation public init(with text: String) { self.text = text baseValue = text + self.titleStateLabel = StateLabelModel(text: "") + self.feedbackStateLabel = StateLabelModel(text: "") } //-------------------------------------------------- @@ -121,10 +140,24 @@ import Foundation hideBorders = try typeContainer.decodeIfPresent(Bool.self, forKey: .hideBorders) ?? false baseValue = text fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) - + titleLabel = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .titleLabel) + feedbackLabel = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .feedbackLabel) if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { self.groupName = groupName } + + //Setup the stateLabelModels + if let titleLabel = titleLabel { + self.titleStateLabel = StateLabelModel(model: titleLabel) + } else { + self.titleStateLabel = StateLabelModel(text: title ?? "") + } + + if let feedBackLabel = feedbackLabel { + self.feedbackStateLabel = StateLabelModel(model: feedBackLabel) + } else { //feedback is the model for the error + self.feedbackStateLabel = StateLabelModel(text: feedback ?? "") + } } public func encode(to encoder: Encoder) throws { @@ -133,7 +166,9 @@ import Foundation try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(title, forKey: .title) + try container.encodeIfPresent(titleLabel, forKey: .titleLabel) try container.encodeIfPresent(feedback, forKey: .feedback) + try container.encodeIfPresent(feedbackLabel, forKey: .feedbackLabel) try container.encodeIfPresent(text, forKey: .text) try container.encodeIfPresent(locked, forKey: .locked) try container.encodeIfPresent(showError, forKey: .showError) @@ -142,6 +177,7 @@ import Foundation try container.encodeIfPresent(errorMessage, forKey: .errorMessage) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) + try container.encode(enabled, forKey: .enabled) try container.encode(hideBorders, forKey: .hideBorders) } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift index a5657dd4..4481411e 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift @@ -423,6 +423,13 @@ import MVMCore checkAndBypassAnimations(selected: model.selected) } + model.updateUI = { [weak self] in + MVMCoreDispatchUtility.performBlock(onMainThread: { + guard let self = self else { return } + self.isEnabled = model.enabled + }) + } + isEnabled = model.enabled if (model.action != nil || model.offAction != nil) { diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift index c4f69f27..69f026ef 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift @@ -13,7 +13,8 @@ var selected: Bool { get set } } -@objcMembers public class CheckboxModel: MoleculeModelProtocol, SelectableMoleculeModelProtocol, FormFieldProtocol { +@objcMembers public class CheckboxModel: MoleculeModelProtocol, SelectableMoleculeModelProtocol, FormFieldProtocol, UIUpdatableModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -22,7 +23,7 @@ public var backgroundColor: Color? public var accessibilityIdentifier: String? public var selected: Bool = false - public var enabled: Bool = true + public var enabled: Bool = true public var animated: Bool = true public var inverted: Bool = false public var round: Bool = false @@ -42,6 +43,7 @@ public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + public var updateUI: ActionBlock? //-------------------------------------------------- // MARK: - Keys @@ -75,7 +77,10 @@ // MARK: - Form Validation //-------------------------------------------------- - public func formFieldValue() -> AnyHashable? { selected } + public func formFieldValue() -> AnyHashable? { + guard enabled else { return nil } + return selected + } //-------------------------------------------------- // MARK: - Initializer diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift index 5d5443d0..f1b22dc3 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift @@ -6,7 +6,7 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -@objcMembers public class RadioBoxModel: MoleculeModelProtocol { +@objcMembers public class RadioBoxModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift index bd1f99d1..05d916e3 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift @@ -20,6 +20,7 @@ public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + public var enabled: Bool = true //-------------------------------------------------- // MARK: - Methods @@ -39,6 +40,7 @@ private enum CodingKeys: String, CodingKey { case moleculeName + case enabled case selectedAccentColor case backgroundColor case accessibilityIdentifier @@ -63,6 +65,9 @@ if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { self.groupName = groupName } + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { + self.enabled = enabled + } baseValue = formFieldValue() } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift index a5000284..71f7a828 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift @@ -17,7 +17,8 @@ private var selectedRadioButton: RadioButton? private var selectedRadioButtonModel: RadioButtonModel? public var baseValue: AnyHashable? - + public var enabled: Bool = true + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -39,6 +40,7 @@ } else { radioButton.isSelected = false } + self.enabled = radioButtonModel.enabled } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift index c961e306..100e2311 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift @@ -7,7 +7,7 @@ // -@objcMembers public class RadioSwatchModel: MoleculeModelProtocol { +@objcMembers public class RadioSwatchModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift index d44b631a..47cb3051 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift @@ -19,7 +19,8 @@ public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? - + public var enabled: Bool = true + //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- @@ -43,6 +44,7 @@ case swatches case fieldKey case groupName + case enabled } //-------------------------------------------------- @@ -58,6 +60,9 @@ if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { self.groupName = groupName } + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { + self.enabled = enabled + } baseValue = formFieldValue() } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index 5be014fb..b4c4c906 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -7,7 +7,7 @@ // -public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableModelProtocol { +public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift index 35dbd906..388f1669 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift @@ -9,7 +9,7 @@ import UIKit -open class ArrowModel: MoleculeModelProtocol { +open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 1a9486f6..9b211857 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -9,7 +9,7 @@ import Foundation -open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelProtocol { +open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 395ab923..687683d7 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -238,7 +238,7 @@ public typealias ActionBlock = () -> () case left } - public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject? = nil, _ additionalData: [AnyHashable: Any]? = nil) { clauses = [] text = nil @@ -584,9 +584,8 @@ public typealias ActionBlock = () -> () /// Will remove the values contained in attributedText. func clearAttributes() { - - guard let labelText = text, - let attributes = attributedText?.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: labelText.count)) + guard let labelText = text, !labelText.isEmpty else { return } + guard let attributes = attributedText?.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: labelText.count)) else { return } let attributedString = NSMutableAttributedString(string: labelText) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/StateLabel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/StateLabel.swift new file mode 100644 index 00000000..d20755c7 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/StateLabel.swift @@ -0,0 +1,70 @@ +// +// StateLabel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/10/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +/// Subclass of label that helps with different states +public class StateLabel: Label { + //properties used in setting label + private var delegateObject: MVMCoreUIDelegateObject? + private var additionalData: [AnyHashable: Any]? + + //models that drive the label UI + private var stateModel: StateLabelModel! + + //public properties + public override var isEnabled: Bool { + didSet{ + self.state = isEnabled ? .enabled : .disabled + } + } + + public override func reset(){ + super.reset() + self.state = .enabled + } + + //current mode of label + public var state: StateLabelModel.State { + get { + return self.stateModel.state + } + set { + self.stateModel.state = newValue + self.set(with: stateModel.model, delegateObject, additionalData) + } + } + + /// Used in setting up the Label for use + /// - Parameters: + /// - text: If there is no Model, there should be a TEXT only version of the label that will be applied to the default font / color styles + /// - model: Model takes priority over a text value. The model has its own text value that will be looked at to draw the screen, this model is used for both enabled/disabled models + /// - delegateObject: passed in from the creator + /// - additionalData: passed in from the creator + public func setup(model: StateLabelModel, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?){ + self.additionalData = additionalData + self.delegateObject = delegateObject + self.stateModel = model + + //default to enabled state + self.reset() + } + + /// Use this to switch the label into a error state + /// - Parameter message: message to show in the errorModel + public func showError(message: String){ + self.stateModel.set(text: message, for: .error) + self.state = .error + } + + /// Text change that will update both enabledModel and disabledModel text values + /// - Parameter text: text you want to see + public func set(text: String?){ + self.stateModel.set(text: text ?? "", for: .enabled) + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/StateLabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/StateLabelModel.swift new file mode 100644 index 00000000..ae6bc317 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/StateLabelModel.swift @@ -0,0 +1,91 @@ +// +// StateLabelModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/14/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class StateLabelModel { + static let defaultFontStyle = Styler.Font.RegularMicro + static let defaultEnabledTextColor = Color(uiColor: .mvmBlack) + static let defaultDisabledTextColor = Color(uiColor: .mvmCoolGray3) + static let defaultErrorTextColor = Color(uiColor: .mvmBlack) + + private var enabledModel: LabelModel + private var disabledModel = LabelModel(fontStyle: StateLabelModel.defaultFontStyle, textColor: StateLabelModel.defaultDisabledTextColor) + private var errorLabelModel = LabelModel(fontStyle: StateLabelModel.defaultFontStyle, textColor: StateLabelModel.defaultErrorTextColor) + + public enum State { + case enabled + case disabled + case error + } + + //current state + public var state: State = .enabled + + //model is based on current state + public var model: LabelModel { + switch state { + case .enabled: + return enabledModel + case .disabled: + return disabledModel + case .error: + return errorLabelModel + } + } + + //init + public init(model: LabelModel){ + + //ensure the fontStyle exist and if so, + //make sure the disabledModel font matches + //otherwise set it to defaultFontStyle + if let modelFontStyle = model.fontStyle { + self.disabledModel.fontStyle = modelFontStyle + } else { + model.fontStyle = StateLabelModel.defaultFontStyle + } + + //ensure the textColor is set + //otherwise use the defaultEnabledTextColor + if model.textColor == nil { + model.textColor = StateLabelModel.defaultEnabledTextColor + } + + //set the enabledModel to the model passed in + self.enabledModel = model + + //make sure the enabled & disabled text match + self.disabledModel.text = self.enabledModel.text + } + + public init(text: String){ + //create the enabled model + self.enabledModel = LabelModel(fontStyle: StateLabelModel.defaultFontStyle, textColor: StateLabelModel.defaultEnabledTextColor) + self.enabledModel.text = text + + //make sure the enabled & disabled text match + self.disabledModel.text = self.enabledModel.text + } + + //methods + public func reset(){ + self.state = .enabled + } + + //set text for state + public func set(text:String, for state: State){ + switch state { + case .enabled, .disabled: + self.enabledModel.text = text + self.disabledModel.text = text + case .error: + self.errorLabelModel.text = text + } + } +} diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index 8721db72..494184bc 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -37,7 +37,8 @@ import UIKit public var baseValue: AnyHashable? public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName - + public var enabled: Bool = true + public var selectable = false public var selectedIndex: Int? diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/DisableableModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/DisableableModelProtocol.swift index db0cfd77..910d707e 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/DisableableModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/DisableableModelProtocol.swift @@ -9,6 +9,6 @@ import Foundation -public protocol EnableableModelProtocol { +public protocol EnableableModelProtocol: AnyObject { var enabled: Bool { get set } } diff --git a/MVMCoreUI/FormUIHelpers/FormFieldEffect/DisableFormFieldEffectModel.swift b/MVMCoreUI/FormUIHelpers/FormFieldEffect/DisableFormFieldEffectModel.swift new file mode 100644 index 00000000..09fbb6e3 --- /dev/null +++ b/MVMCoreUI/FormUIHelpers/FormFieldEffect/DisableFormFieldEffectModel.swift @@ -0,0 +1,62 @@ +// +// DisableFormFieldEffectModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/6/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class DisableFormFieldEffectModel: FormFieldEffectProtocol { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String = "disableFormFieldEffect" + public var fieldKey: String = "" + public var activatedRuleIds: [String]? + public var rules: [RulesProtocol] + + init(_ fieldKey: String, _ activatedRuleIds: [String], _ rules: [RulesProtocol]) { + self.fieldKey = fieldKey + self.activatedRuleIds = activatedRuleIds + self.rules = rules + } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case fieldKey + case activatedRuleIds + case rules + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + self.fieldKey = try typeContainer.decode(String.self, forKey: .fieldKey) + self.activatedRuleIds = try typeContainer.decodeIfPresent([String].self, forKey: .activatedRuleIds) + self.rules = try typeContainer.decodeModels(codingKey: .rules) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(fieldKey, forKey: .fieldKey) + try container.encode(activatedRuleIds, forKey: .activatedRuleIds) + try container.encodeModels(rules, forKey: .rules) + } + + public func setEffect(validity: Bool, field: FormFieldProtocol, form: FormValidator, group: FormGroupRule) { + field.enabled = !validity + if let updateField = field as? UIUpdatableModelProtocol { + updateField.updateUI?() + } + } +} diff --git a/MVMCoreUI/FormUIHelpers/FormFieldEffect/EnableFormFieldEffectModel.swift b/MVMCoreUI/FormUIHelpers/FormFieldEffect/EnableFormFieldEffectModel.swift new file mode 100644 index 00000000..f4537bb6 --- /dev/null +++ b/MVMCoreUI/FormUIHelpers/FormFieldEffect/EnableFormFieldEffectModel.swift @@ -0,0 +1,64 @@ +// +// EnableFormFieldEffectModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/1/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + + +public class EnableFormFieldEffectModel: FormFieldEffectProtocol { + + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String = "enableFormFieldEffect" + public var fieldKey: String = "" + public var activatedRuleIds: [String]? + public var rules: [RulesProtocol] + + init(_ fieldKey: String, activatedRuleIds: [String], rules: [RulesProtocol]) { + self.fieldKey = fieldKey + self.activatedRuleIds = activatedRuleIds + self.rules = rules + } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case fieldKey + case activatedRuleIds + case rules + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + self.fieldKey = try typeContainer.decode(String.self, forKey: .fieldKey) + self.activatedRuleIds = try typeContainer.decodeIfPresent([String].self, forKey: .activatedRuleIds) + self.rules = try typeContainer.decodeModels(codingKey: .rules) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(fieldKey, forKey: .fieldKey) + try container.encode(activatedRuleIds, forKey: .activatedRuleIds) + try container.encodeModels(rules, forKey: .rules) + } + + public func setEffect(validity: Bool, field: FormFieldProtocol,form: FormValidator, group: FormGroupRule) { + field.enabled = validity + if let updateField = field as? UIUpdatableModelProtocol { + updateField.updateUI?() + } + } +} diff --git a/MVMCoreUI/FormUIHelpers/FormFieldEffect/FormFieldEffectProtocol.swift b/MVMCoreUI/FormUIHelpers/FormFieldEffect/FormFieldEffectProtocol.swift new file mode 100644 index 00000000..828f9e2d --- /dev/null +++ b/MVMCoreUI/FormUIHelpers/FormFieldEffect/FormFieldEffectProtocol.swift @@ -0,0 +1,34 @@ +// +// FormFieldEffectProtocol.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/6/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public protocol FormFieldEffectProtocol: ModelProtocol, RulesContainerProtocol { + + /// This is the key to the form field that will take the effect + var fieldKey: String { get } + + /// This is an array of ruleIds that are a property of RulesProtocol for the array of Validation rules within the FormGroupRule.rules + /// When this effect "rules" are valid (true), the Form will add the fieldKey to the corresponding Validation rule "fields" property. + /// When this effect "rules" are invalid (false), the Form will remove the fieldKey from the corresponding Validation rule "fields" property. + var activatedRuleIds: [String]? { get } + + /// This function will execute the effect on the field passed in based on the validity. validity is the result of the array of rules run. + /// - Parameters: + /// - validity: This is run against the FormFieldAffect + /// - field: Form Field that is bound to the fieldKey + /// - form: Form that the formGroup is attached + /// - group: FormGroupRule that is attached to this effect + func setEffect(validity: Bool, field: FormFieldProtocol, form: FormValidator, group: FormGroupRule) +} + +public extension FormFieldEffectProtocol { + var type: String { Self.identifier } + static var categoryCodingKey: String { "type" } + static var categoryName: String { "\(FormFieldEffectProtocol.self)" } +} diff --git a/MVMCoreUI/FormUIHelpers/FormFieldEffect/HideFormFieldEffectModel.swift b/MVMCoreUI/FormUIHelpers/FormFieldEffect/HideFormFieldEffectModel.swift new file mode 100644 index 00000000..451dbe83 --- /dev/null +++ b/MVMCoreUI/FormUIHelpers/FormFieldEffect/HideFormFieldEffectModel.swift @@ -0,0 +1,62 @@ +// +// HideFormFieldEffectModel.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/6/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class HideFormFieldEffectModel: FormFieldEffectProtocol { + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public static var identifier: String = "hideFormFieldEffect" + public var fieldKey: String = "" + public var activatedRuleIds: [String]? + public var rules: [RulesProtocol] + + init(_ fieldKey: String, _ activatedRuleIds: [String], _ rules: [RulesProtocol]) { + self.fieldKey = fieldKey + self.activatedRuleIds = activatedRuleIds + self.rules = rules + } + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + + private enum CodingKeys: String, CodingKey { + case fieldKey + case activatedRuleIds + case rules + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + self.fieldKey = try typeContainer.decode(String.self, forKey: .fieldKey) + self.activatedRuleIds = try typeContainer.decodeIfPresent([String].self, forKey: .activatedRuleIds) + self.rules = try typeContainer.decodeModels(codingKey: .rules) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(fieldKey, forKey: .fieldKey) + try container.encode(activatedRuleIds, forKey: .activatedRuleIds) + try container.encodeModels(rules, forKey: .rules) + } + + public func setEffect(validity: Bool, field: FormFieldProtocol, form: FormValidator, group: FormGroupRule) { + field.enabled = validity + if let updateField = field as? UIUpdatableModelProtocol { + updateField.updateUI?() + } + } +} diff --git a/MVMCoreUI/FormUIHelpers/FormGroupWatcherFieldProtocol.swift b/MVMCoreUI/FormUIHelpers/FormGroupWatcherFieldProtocol.swift index 624ea374..6219b0b5 100644 --- a/MVMCoreUI/FormUIHelpers/FormGroupWatcherFieldProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/FormGroupWatcherFieldProtocol.swift @@ -9,6 +9,11 @@ import Foundation -public protocol FormGroupWatcherFieldProtocol: FormItemProtocol { - func setValidity(_ valid: Bool, group: FormGroupRule) +public protocol FormGroupWatcherFieldProtocol: FormItemProtocol, UIUpdatableModelProtocol {} + +public extension FormGroupWatcherFieldProtocol { + func setValidity(_ valid: Bool){ + self.enabled = valid; + updateUI?() + } } diff --git a/MVMCoreUI/FormUIHelpers/FormItemProtocol.swift b/MVMCoreUI/FormUIHelpers/FormItemProtocol.swift index 0c7f71bd..081f736e 100644 --- a/MVMCoreUI/FormUIHelpers/FormItemProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/FormItemProtocol.swift @@ -9,7 +9,7 @@ import Foundation -public protocol FormItemProtocol { +public protocol FormItemProtocol: EnableableModelProtocol { /// The group name to used to group this item for validation, enableability, and value getting. var groupName: String { get set } } diff --git a/MVMCoreUI/FormUIHelpers/FormValidator.swift b/MVMCoreUI/FormUIHelpers/FormValidator.swift index 297d1f9e..f991926b 100644 --- a/MVMCoreUI/FormUIHelpers/FormValidator.swift +++ b/MVMCoreUI/FormUIHelpers/FormValidator.swift @@ -11,6 +11,11 @@ import MVMCore @objcMembers public class FormValidator: NSObject { + + public enum ValidationError: Error { + case other(error: String) + } + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -80,38 +85,115 @@ import MVMCore /// Validates all rule groups. Returns if valid public func validate() -> Bool { - var valid = true guard let formRules = formRules else { return valid } for group in formRules { - let groupValid = validateGroup(group) - valid = valid && groupValid + + var groupValid = false + do { + groupValid = try validateGroup(group) + } catch { + if let err = MVMCoreErrorObject.createErrorObject(for: error, location: "FormValidator"){ + MVMCoreLoggingHandler.shared()?.addError(toLog: err) + fatalError(err.description) + } + } + valid = valid && groupValid } return valid } - - /// Validates a given rule group. Returns if valid - public func validateGroup(_ group: FormGroupRule) -> Bool { - // Validate each rule. - var valid = true - var previousValidity: [String: Bool] = [:] - for rule in group.rules { - let tuple = rule.validate(fields, previousValidity) - let isValidRule = tuple.valid - let returnedValidity = tuple.fieldValidity - previousValidity = previousValidity.merging(returnedValidity) { (_, new) in new } - valid = valid && isValidRule - } + + /// Validates a FormGroupRule + /// - Parameters: + /// - group: FormGroupRule being validated + /// - counter: keeps track of how many times causes another group validation + /// - Returns: validity for the FormGroupRule.rules + public func validateGroup(_ group: FormGroupRule, counter: Int = 0) throws -> Bool { + let valid = group.validate(fields) // Notify the group watchers of validity. - for watcher in groupWatchers { - if watcher.groupName == group.groupName { - watcher.setValidity(valid, group: group) + for watcher in groupWatchers.filter({$0.groupName == group.groupName}) { + watcher.setValidity(valid) + } + + var ruleChange = false + + //loop the effects + group.effects?.forEach({ effect in + //get the fieldKey for the effect + if let effected = fields[effect.fieldKey] { + //get the validity + let validity = effect.validate(fields) + + //set the effect with the validation + effect.setEffect(validity: validity, field: effected, form: self, group: group) + + //update the group form rules + if let ruleIds = effect.activatedRuleIds { + let didChange = self.updateRules(for: group, with: validity, for: effect.fieldKey, and: ruleIds) + if(didChange) { + ruleChange = didChange + } + } + } + }) + + if ruleChange { + if counter > 3 { + throw ValidationError.other(error: "Effect caused validation loop error") + } else { + return try self.validateGroup(group, counter: counter + 1) + } + } else { + return valid + } + + } + + /// Updates the Fields in which a specific rule within a FormGroupRule validates against. + /// - Parameters: + /// - group: FormGroupRule to update + /// - validity: If you the fieldKey should be added or removed from a rule + /// - fieldKey: FieldKey that will be added or removed from a rule + /// - ruleIds: Array of ruleIds to add or remove the fieldKey into the rule.fields property + public func updateRules(for group: FormGroupRule, with validity: Bool, for fieldKey: String, and ruleIds: [String]) -> Bool{ + //update the group rules based on the validation of this rule to show/hide + var ruleChange = false + ruleIds.forEach { ruleId in + //only change a rule found + if var mutableRule = group.rules.first(where: { rule in return rule.ruleId == ruleId}) { + //make a copy of the fields + var mutableFields = mutableRule.fields + //if the validity is true + if validity { + //we need to ensure this fieldKey is included in the found rule + //therefore we mush append the fieldKey to the fields array + if !mutableRule.fields.contains(where: { field in return field == fieldKey }){ + mutableFields.append(fieldKey) + mutableRule.fields = mutableFields + //a rule was changed, therefore we need + //to re-run the validation on the group + ruleChange = true + } + } else { + //the validity is false, therefore we must ensure + //that the fieldKey does NOT exist in the found + //rule fields + if let fieldKeyIndex = mutableFields.firstIndex(of: fieldKey) { + mutableFields.remove(at: fieldKeyIndex) + mutableRule.fields = mutableFields + //a rule was changed, therefore we need + //to re-run the validation on the group + ruleChange = true + } + } } } - return valid + + //if any rules updated, run the validateGroup again + return ruleChange } } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/FormGroupRule.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/FormGroupRule.swift index 56a5a2cf..ee6ca7de 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/FormGroupRule.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/FormGroupRule.swift @@ -9,22 +9,22 @@ import Foundation - -open class FormGroupRule: Codable { +open class FormGroupRule: RulesContainerProtocol, Codable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - var groupName: String - var rules: [RulesProtocol] - + public var groupName: String + public var rules: [RulesProtocol] + public var effects: [FormFieldEffectProtocol]?; //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- - init(_ groupName: String, _ rules: [RulesProtocol]) { + public init(_ groupName: String, _ rules: [RulesProtocol], _ effects: [FormFieldEffectProtocol]) { self.groupName = groupName self.rules = rules + self.effects = effects } //-------------------------------------------------- @@ -34,6 +34,7 @@ open class FormGroupRule: Codable { private enum CodingKeys: String, CodingKey { case groupName case rules + case effects } //-------------------------------------------------- @@ -44,6 +45,7 @@ open class FormGroupRule: Codable { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) self.groupName = try typeContainer.decode(String.self, forKey: .groupName) self.rules = try typeContainer.decodeModels(codingKey: .rules) + self.effects = try typeContainer.decodeModelsIfPresent(codingKey: .effects) } public func encode(to encoder: Encoder) throws { diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift index aa80f5b0..69b8599b 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAllValueChangedModel.swift @@ -15,6 +15,7 @@ public class RuleAllValueChangedModel: RulesProtocol { public static var identifier: String = "allValueChanged" public var type: String = RuleAllValueChangedModel.identifier + public var ruleId: String? public var errorMessage: [String: String]? public var fields: [String] diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift index 67f1d754..2b958d3b 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyRequiredModel.swift @@ -16,6 +16,7 @@ public class RuleAnyRequiredModel: RulesProtocol { public static var identifier: String = "anyRequired" public var type: String = RuleRequiredModel.identifier + public var ruleId: String? public var fields: [String] public var errorMessage: [String: String]? diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift index cd8e2eda..09e91ceb 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleAnyValueChangedModel.swift @@ -16,6 +16,7 @@ public class RuleAnyValueChangedModel: RulesProtocol { public static var identifier: String = "anyValueChanged" public var type: String = RuleAnyValueChangedModel.identifier + public var ruleId: String? public var errorMessage: [String: String]? public var fields: [String] diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift index 8326cf4f..dd02070b 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsIgnoreCaseModel.swift @@ -16,6 +16,7 @@ public class RuleEqualsIgnoreCaseModel: RulesProtocol { public static var identifier: String = "equalsIgnoreCase" public var type: String = RuleEqualsIgnoreCaseModel.identifier + public var ruleId: String? public var fields: [String] public var errorMessage: [String: String]? diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift index a405e2e7..fae81358 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleEqualsModel.swift @@ -16,6 +16,7 @@ public class RuleEqualsModel: RulesProtocol { public static var identifier: String = "equals" public var type: String = RuleEqualsModel.identifier + public var ruleId: String? public var fields: [String] public var errorMessage: [String: String]? diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift index 98bec293..48e4582b 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRegexModel.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // - public class RuleRegexModel: RulesProtocol { //-------------------------------------------------- // MARK: - Properties @@ -14,6 +13,7 @@ public class RuleRegexModel: RulesProtocol { public static var identifier: String = "regex" public var type: String = RuleRegexModel.identifier + public var ruleId: String? public var fields: [String] public var regex: String public var errorMessage: [String: String]? @@ -30,4 +30,5 @@ public class RuleRegexModel: RulesProtocol { return false } + } diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift index 77cdf254..f401721b 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RuleRequiredModel.swift @@ -16,6 +16,7 @@ public class RuleRequiredModel: RulesProtocol { public static var identifier: String = "allRequired" public var type: String = RuleRequiredModel.identifier + public var ruleId: String? public var errorMessage: [String: String]? public var fields: [String] diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift index db8614ac..37cd1828 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift @@ -16,7 +16,11 @@ public enum RulesCodingKey: String, CodingKey { public protocol RulesProtocol: ModelProtocol { // The type of rule var type: String { get } - + + //id for the rule + var ruleId: String? { get } + + //error message to show for the rule var errorMessage: [String: String]? { get } // The fields that this rule applies to. @@ -56,3 +60,25 @@ public extension RulesProtocol { return (valid: valid, fieldValidity: previousValidity) } } + +public protocol RulesContainerProtocol{ + var rules: [RulesProtocol] { get } +} + +public extension RulesContainerProtocol { + func validate(_ fields: [String: FormFieldProtocol]) -> Bool { + // Validate each rule. + var valid = true + var previousValidity: [String: Bool] = [:] + for rule in self.rules { + + //validate the rule against the fields + let tuple = rule.validate(fields, previousValidity) + + //merge the fieldValidity + previousValidity = previousValidity.merging(tuple.fieldValidity) { (_, new) in new } + valid = valid && tuple.valid + } + return valid + } +} diff --git a/MVMCoreUI/FormUIHelpers/UIUpdatableModelProtocol.swift b/MVMCoreUI/FormUIHelpers/UIUpdatableModelProtocol.swift new file mode 100644 index 00000000..e67ae896 --- /dev/null +++ b/MVMCoreUI/FormUIHelpers/UIUpdatableModelProtocol.swift @@ -0,0 +1,13 @@ +// +// Updatable.swift +// MVMCoreUI +// +// Created by Matt Bruce on 12/1/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public protocol UIUpdatableModelProtocol { + var updateUI: ActionBlock? { get set } +} diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 78dd2916..11328440 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -243,5 +243,8 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(RuleEqualsModel.self) ModelRegistry.register(RuleEqualsIgnoreCaseModel.self) ModelRegistry.register(RuleRegexModel.self) + ModelRegistry.register(EnableFormFieldEffectModel.self) + ModelRegistry.register(DisableFormFieldEffectModel.self) + ModelRegistry.register(HideFormFieldEffectModel.self) } }