Merge branch 'develop' of https://gitlab.verizon.com/BPHV_MIPS/mvm_core_ui.git into feature/atomic-vds-toggle
This commit is contained in:
commit
eb9ad769e5
@ -153,6 +153,9 @@
|
|||||||
444FB7C32821B76B00DFE692 /* TitleLockupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */; };
|
444FB7C32821B76B00DFE692 /* TitleLockupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */; };
|
||||||
4457904E27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */; };
|
4457904E27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */; };
|
||||||
4B002ACA2BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */; };
|
4B002ACA2BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */; };
|
||||||
|
4B3408A22C3873B0003BFABF /* CircularProgressBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */; };
|
||||||
|
4B3408A42C3873E8003BFABF /* CircularProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */; };
|
||||||
|
4B53AF7B2C45BBBA00274685 /* GraphSizeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */; };
|
||||||
522679C123FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */; };
|
522679C123FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */; };
|
||||||
522679C223FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */; };
|
522679C223FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */; };
|
||||||
52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */; };
|
52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */; };
|
||||||
@ -578,6 +581,8 @@
|
|||||||
EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; };
|
EA17584A2BC97EF100A5C0D9 /* BadgeIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */; };
|
||||||
EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; };
|
EA17584C2BC9894800A5C0D9 /* ButtonIconModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */; };
|
||||||
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; };
|
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */; };
|
||||||
|
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */; };
|
||||||
|
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */; };
|
||||||
EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; };
|
EA41F4AC2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */; };
|
||||||
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; };
|
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */; };
|
||||||
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; };
|
EA5124FF2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */; };
|
||||||
@ -771,6 +776,9 @@
|
|||||||
444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupModel.swift; sourceTree = "<group>"; };
|
444FB7C22821B76B00DFE692 /* TitleLockupModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupModel.swift; sourceTree = "<group>"; };
|
||||||
4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageRenderingMode+Extension.swift"; sourceTree = "<group>"; };
|
4457904D27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageRenderingMode+Extension.swift"; sourceTree = "<group>"; };
|
||||||
4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateDropdownEntryFieldModel+Extension.swift"; sourceTree = "<group>"; };
|
4B002AC92BD855EC009BC9C1 /* DateDropdownEntryFieldModel+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateDropdownEntryFieldModel+Extension.swift"; sourceTree = "<group>"; };
|
||||||
|
4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBarModel.swift; sourceTree = "<group>"; };
|
||||||
|
4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressBar.swift; sourceTree = "<group>"; };
|
||||||
|
4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphSizeProtocol.swift; sourceTree = "<group>"; };
|
||||||
522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinks.swift; sourceTree = "<group>"; };
|
522679BF23FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinks.swift; sourceTree = "<group>"; };
|
||||||
522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinksModel.swift; sourceTree = "<group>"; };
|
522679C023FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListLeftVariableCheckboxAllTextAndLinksModel.swift; sourceTree = "<group>"; };
|
||||||
52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextAllTextAndLinks.swift; sourceTree = "<group>"; };
|
52267A0623FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListOneColumnFullWidthTextAllTextAndLinks.swift; sourceTree = "<group>"; };
|
||||||
@ -1199,6 +1207,8 @@
|
|||||||
EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
EA1758492BC97EF100A5C0D9 /* BadgeIndicatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeIndicatorModel.swift; sourceTree = "<group>"; };
|
||||||
EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = "<group>"; };
|
EA17584B2BC9894800A5C0D9 /* ButtonIconModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIconModel.swift; sourceTree = "<group>"; };
|
||||||
EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
|
EA17584D2BC9895A00A5C0D9 /* ButtonIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonIcon.swift; sourceTree = "<group>"; };
|
||||||
|
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleVDSModel.swift; sourceTree = "<group>"; };
|
||||||
|
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputEntryField.swift; sourceTree = "<group>"; };
|
||||||
EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
EA41F4AB2787927100F5B377 /* DynamicRuleFormFieldEffectModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRuleFormFieldEffectModel.swift; sourceTree = "<group>"; };
|
||||||
EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = "<group>"; };
|
EA5124FC243601600051A3A4 /* BGImageHeadlineBodyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButton.swift; sourceTree = "<group>"; };
|
||||||
EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
|
EA5124FE2436018E0051A3A4 /* BGImageHeadlineBodyButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageHeadlineBodyButtonModel.swift; sourceTree = "<group>"; };
|
||||||
@ -1296,6 +1306,7 @@
|
|||||||
011D95A0240453D0000E3791 /* RuleEqualsModel.swift */,
|
011D95A0240453D0000E3791 /* RuleEqualsModel.swift */,
|
||||||
0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */,
|
0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */,
|
||||||
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */,
|
FD9912FF28E21E4900542CC3 /* RuleNotEqualsModel.swift */,
|
||||||
|
EA1B02DD2C41BFD200F0758B /* RuleVDSModel.swift */,
|
||||||
);
|
);
|
||||||
name = Rules;
|
name = Rules;
|
||||||
path = Rules/Rules;
|
path = Rules/Rules;
|
||||||
@ -2311,8 +2322,11 @@
|
|||||||
94C2D9822386F3E30006CF46 /* Label */,
|
94C2D9822386F3E30006CF46 /* Label */,
|
||||||
31BE15C923D8924C00452370 /* CheckboxLabelModel.swift */,
|
31BE15C923D8924C00452370 /* CheckboxLabelModel.swift */,
|
||||||
0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */,
|
0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */,
|
||||||
|
4B53AF7A2C45BBBA00274685 /* GraphSizeProtocol.swift */,
|
||||||
D28A838223CCBD3F00DFE4FC /* WheelModel.swift */,
|
D28A838223CCBD3F00DFE4FC /* WheelModel.swift */,
|
||||||
943784F3236B77BB006A1E82 /* Wheel.swift */,
|
943784F3236B77BB006A1E82 /* Wheel.swift */,
|
||||||
|
4B3408A12C3873B0003BFABF /* CircularProgressBarModel.swift */,
|
||||||
|
4B3408A32C3873E8003BFABF /* CircularProgressBar.swift */,
|
||||||
943784F4236B77BB006A1E82 /* WheelAnimationHandler.swift */,
|
943784F4236B77BB006A1E82 /* WheelAnimationHandler.swift */,
|
||||||
0AE98BB623FF18E9004C5109 /* ArrowModel.swift */,
|
0AE98BB623FF18E9004C5109 /* ArrowModel.swift */,
|
||||||
0AE98BB423FF18D2004C5109 /* Arrow.swift */,
|
0AE98BB423FF18D2004C5109 /* Arrow.swift */,
|
||||||
@ -2348,6 +2362,7 @@
|
|||||||
children = (
|
children = (
|
||||||
0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */,
|
0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */,
|
||||||
0A21DB7E235DECC500C160A2 /* EntryField.swift */,
|
0A21DB7E235DECC500C160A2 /* EntryField.swift */,
|
||||||
|
EA1B02DF2C470AFD00F0758B /* InputEntryField.swift */,
|
||||||
0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */,
|
0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */,
|
||||||
0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */,
|
0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */,
|
||||||
0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */,
|
0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */,
|
||||||
@ -2836,6 +2851,7 @@
|
|||||||
D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */,
|
D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */,
|
||||||
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */,
|
EAB14BC127D935F00012AB2C /* RuleCompareModelProtocol.swift in Sources */,
|
||||||
011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */,
|
011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */,
|
||||||
|
EA1B02DE2C41BFD200F0758B /* RuleVDSModel.swift in Sources */,
|
||||||
EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */,
|
EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */,
|
||||||
BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */,
|
BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */,
|
||||||
D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */,
|
D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */,
|
||||||
@ -3008,6 +3024,7 @@
|
|||||||
D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */,
|
D29DF2EF21ECEAE1003B2FB9 /* MFFonts.m in Sources */,
|
||||||
D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */,
|
D22479942316AE5E003FCCF9 /* NSLayoutConstraintExtension.swift in Sources */,
|
||||||
D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */,
|
D2B18B94236214AD00A9AEDC /* NavigationController.swift in Sources */,
|
||||||
|
4B3408A42C3873E8003BFABF /* CircularProgressBar.swift in Sources */,
|
||||||
0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */,
|
0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */,
|
||||||
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */,
|
EA17584E2BC9895A00A5C0D9 /* ButtonIcon.swift in Sources */,
|
||||||
D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */,
|
D29E28DA23D21AFA00ACEA85 /* StringAndMoleculeModel.swift in Sources */,
|
||||||
@ -3028,6 +3045,7 @@
|
|||||||
AA1EC59924373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift in Sources */,
|
AA1EC59924373994003D6F50 /* ListThreeColumnSpeedTestDivider.swift in Sources */,
|
||||||
AA37CBD52519072F0027344C /* Stars.swift in Sources */,
|
AA37CBD52519072F0027344C /* Stars.swift in Sources */,
|
||||||
942C378E2412F5B60066E45E /* ModalMoleculeStackTemplate.swift in Sources */,
|
942C378E2412F5B60066E45E /* ModalMoleculeStackTemplate.swift in Sources */,
|
||||||
|
4B3408A22C3873B0003BFABF /* CircularProgressBarModel.swift in Sources */,
|
||||||
8D8067D32444473A00203BE8 /* ListRightVariablePriceChangeAllTextAndLinks.swift in Sources */,
|
8D8067D32444473A00203BE8 /* ListRightVariablePriceChangeAllTextAndLinks.swift in Sources */,
|
||||||
8D4687E4242E2DF300802879 /* ListFourColumnDataUsageListItem.swift in Sources */,
|
8D4687E4242E2DF300802879 /* ListFourColumnDataUsageListItem.swift in Sources */,
|
||||||
D2874024249BA6F300BE950A /* MVMCoreUISplitViewController+Extension.swift in Sources */,
|
D2874024249BA6F300BE950A /* MVMCoreUISplitViewController+Extension.swift in Sources */,
|
||||||
@ -3104,6 +3122,7 @@
|
|||||||
D2A6390522CBCE160052ED1F /* MoleculeCollectionViewCell.swift in Sources */,
|
D2A6390522CBCE160052ED1F /* MoleculeCollectionViewCell.swift in Sources */,
|
||||||
D2A6390122CBB1820052ED1F /* Carousel.swift in Sources */,
|
D2A6390122CBB1820052ED1F /* Carousel.swift in Sources */,
|
||||||
C7F8012123E8303200396FBD /* ListRVWheel.swift in Sources */,
|
C7F8012123E8303200396FBD /* ListRVWheel.swift in Sources */,
|
||||||
|
4B53AF7B2C45BBBA00274685 /* GraphSizeProtocol.swift in Sources */,
|
||||||
BB2C968F24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift in Sources */,
|
BB2C968F24330EA7006FF80C /* ListRightVariableTextLinkAllTextAndLinksModel.swift in Sources */,
|
||||||
D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */,
|
D2FB151B23A2B65B00C20E10 /* MoleculeContainer.swift in Sources */,
|
||||||
EA7D81622B2B6E7F00D29F9E /* IconModel.swift in Sources */,
|
EA7D81622B2B6E7F00D29F9E /* IconModel.swift in Sources */,
|
||||||
@ -3136,6 +3155,7 @@
|
|||||||
323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */,
|
323AC96A24C837F000F8E4C4 /* ListThreeColumnBillChangesModel.swift in Sources */,
|
||||||
D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */,
|
D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */,
|
||||||
525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */,
|
525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */,
|
||||||
|
EA1B02E02C470AFD00F0758B /* InputEntryField.swift in Sources */,
|
||||||
D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */,
|
D28BA730247EC2EB00B75CB8 /* NavigationButtonModelProtocol.swift in Sources */,
|
||||||
0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */,
|
0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */,
|
||||||
D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */,
|
D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */,
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import VDS
|
|||||||
public var readOnly: Bool = false
|
public var readOnly: Bool = false
|
||||||
public var showError: Bool?
|
public var showError: Bool?
|
||||||
public var errorMessage: String?
|
public var errorMessage: String?
|
||||||
|
public var initialErrorMessage: String?
|
||||||
|
|
||||||
public var fieldKey: String?
|
public var fieldKey: String?
|
||||||
public var groupName: String = FormValidator.defaultGroupName
|
public var groupName: String = FormValidator.defaultGroupName
|
||||||
@ -89,9 +90,11 @@ import VDS
|
|||||||
|
|
||||||
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
if let ruleErrorMessage = errorMessage, fieldKey != nil {
|
||||||
self.errorMessage = ruleErrorMessage
|
self.errorMessage = ruleErrorMessage
|
||||||
|
} else {
|
||||||
|
self.errorMessage = initialErrorMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isValid = valid
|
isValid = valid
|
||||||
updateUI?()
|
updateUI?()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,6 +106,7 @@ import VDS
|
|||||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||||
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier)
|
||||||
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage)
|
||||||
|
initialErrorMessage = errorMessage
|
||||||
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
|
||||||
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
required = try typeContainer.decodeIfPresent(Bool.self, forKey: .required) ?? true
|
||||||
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
readOnly = try typeContainer.decodeIfPresent(Bool.self, forKey: .readOnly) ?? false
|
||||||
|
|||||||
@ -345,9 +345,8 @@ import UIKit
|
|||||||
|
|
||||||
numberOfDigits = model.digits
|
numberOfDigits = model.digits
|
||||||
|
|
||||||
if let entryType = model.type {
|
let entryType = model.type
|
||||||
setAsSecureTextEntry(entryType == .secure || entryType == .password)
|
setAsSecureTextEntry(entryType == .secure || entryType == .password)
|
||||||
}
|
|
||||||
|
|
||||||
let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self
|
let observingDelegate = delegateObject?.observingTextFieldDelegate ?? self
|
||||||
|
|
||||||
|
|||||||
@ -7,17 +7,64 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
open class ItemDropdownEntryField: VDS.DropdownSelect, VDSMoleculeViewProtocol, ObservingTextFieldDelegate {
|
||||||
|
//------------------------------------------------------
|
||||||
|
// MARK: - Properties
|
||||||
|
//------------------------------------------------------
|
||||||
|
open var viewModel: ItemDropdownEntryFieldModel!
|
||||||
|
open var delegateObject: MVMCoreUIDelegateObject?
|
||||||
|
open var additionalData: [AnyHashable : Any]?
|
||||||
|
|
||||||
|
// Form Validation
|
||||||
|
var fieldKey: String?
|
||||||
|
var fieldValue: JSONValue?
|
||||||
|
var groupName: String?
|
||||||
|
|
||||||
|
open var pickerData: [String] = [] {
|
||||||
|
didSet {
|
||||||
|
options = pickerData.compactMap({ DropdownOptionModel(text: $0) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isEditting: Bool = false
|
||||||
|
|
||||||
open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
public var isValid: Bool = true
|
||||||
|
|
||||||
open var pickerData: [String] = []
|
/// Closure passed here will run as picker changes items.
|
||||||
|
public var observeDropdownChange: ((String?, String) -> ())?
|
||||||
|
|
||||||
public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? {
|
/// Closure passed here will run upon dismissing the selection picker.
|
||||||
model as? ItemDropdownEntryFieldModel
|
public var observeDropdownSelection: ((String) -> ())?
|
||||||
|
|
||||||
|
/// When selecting for first responder, allow initial selected value to appear in empty text field.
|
||||||
|
public var setInitialValueInTextField = true
|
||||||
|
|
||||||
|
open override var errorText: String? {
|
||||||
|
get {
|
||||||
|
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Delegate Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||||
|
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||||
|
|
||||||
|
/// If you're using a ViewController, you must set this to it
|
||||||
|
open weak var uiTextFieldDelegate: UITextFieldDelegate? {
|
||||||
|
get { dropdownField.delegate }
|
||||||
|
set { dropdownField.delegate = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||||
|
_ = resignFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -28,7 +75,7 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
|||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public convenience init() {
|
@objc public convenience required init() {
|
||||||
self.init(frame: .zero)
|
self.init(frame: .zero)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,75 +88,133 @@ open class ItemDropdownEntryField: BaseItemPickerEntryField {
|
|||||||
fatalError("ItemDropdownEntryField init(coder:) has not been implemented")
|
fatalError("ItemDropdownEntryField init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
|
||||||
super.init(model: model, delegateObject, additionalData)
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open override func setup() {
|
||||||
|
super.setup()
|
||||||
|
useRequiredRule = false
|
||||||
|
|
||||||
|
publisher(for: .valueChanged)
|
||||||
|
.sink { [weak self] control in
|
||||||
|
guard let self, let selectedItem else { return }
|
||||||
|
viewModel.selectedIndex = control.selectId
|
||||||
|
observeDropdownSelection?(selectedItem.text)
|
||||||
|
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
|
dropdownField
|
||||||
|
.publisher(for: .editingDidBegin)
|
||||||
|
.sink { [weak self] textField in
|
||||||
|
guard let self else { return }
|
||||||
|
isEditting = true
|
||||||
|
setInitialValueFromPicker()
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
|
dropdownField
|
||||||
|
.publisher(for: .editingDidEnd)
|
||||||
|
.sink { [weak self] textField in
|
||||||
|
guard let self else { return }
|
||||||
|
isEditting = false
|
||||||
|
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||||
|
if let valid = viewModel.isValid {
|
||||||
|
updateValidation(valid)
|
||||||
|
}
|
||||||
|
performDropdownAction()
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func viewModelDidUpdate() {
|
||||||
|
pickerData = viewModel.options
|
||||||
|
showInlineLabel = viewModel.showInlineLabel
|
||||||
|
helperTextPlacement = viewModel.feedbackTextPlacement
|
||||||
|
labelText = viewModel.title
|
||||||
|
helperText = viewModel.feedback
|
||||||
|
isEnabled = viewModel.enabled
|
||||||
|
isReadOnly = viewModel.readOnly
|
||||||
|
isRequired = viewModel.required
|
||||||
|
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||||
|
width = viewModel.width
|
||||||
|
transparentBackground = viewModel.transparentBackground
|
||||||
|
|
||||||
|
if let index = viewModel.selectedIndex {
|
||||||
|
selectId = index
|
||||||
|
optionsPicker.selectRow(index, inComponent: 0, animated: false)
|
||||||
|
pickerView(optionsPicker, didSelectRow: index, inComponent: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||||
|
|
||||||
|
viewModel.wasInitiallySelected = true
|
||||||
|
isEditting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||||
|
if isEditting {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
_ = self.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.updateUI = {
|
||||||
|
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if isEditting {
|
||||||
|
updateValidation(viewModel.isValid ?? true)
|
||||||
|
|
||||||
|
} else if viewModel.isValid ?? true && showError {
|
||||||
|
showError = false
|
||||||
|
}
|
||||||
|
isEnabled = viewModel.enabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.updateUIDynamicError = {
|
||||||
|
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let validState = viewModel.isValid ?? false
|
||||||
|
if !validState && viewModel.shouldClearText {
|
||||||
|
selectId = nil
|
||||||
|
viewModel.shouldClearText = false
|
||||||
|
}
|
||||||
|
updateValidation(validState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateView(_ size: CGFloat) { }
|
||||||
|
|
||||||
/// Sets the textField with the first value of the available picker data.
|
/// Sets the textField with the first value of the available picker data.
|
||||||
@objc private func setInitialValueFromPicker() {
|
private func setInitialValueFromPicker() {
|
||||||
|
|
||||||
guard !pickerData.isEmpty else { return }
|
guard !pickerData.isEmpty else { return }
|
||||||
|
|
||||||
if setInitialValueInTextField {
|
if setInitialValueInTextField {
|
||||||
let pickerIndex = pickerView.selectedRow(inComponent: 0)
|
let pickerIndex = optionsPicker.selectedRow(inComponent: 0)
|
||||||
itemDropdownEntryFieldModel?.selectedIndex = pickerIndex
|
viewModel.selectedIndex = pickerIndex
|
||||||
observeDropdownChange?(text, pickerData[pickerIndex])
|
selectId = pickerIndex
|
||||||
text = pickerData[pickerIndex]
|
observeDropdownChange?(selectedItem?.text, pickerData[pickerIndex])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc override func startEditing() {
|
private func performDropdownAction() {
|
||||||
super.startEditing()
|
guard let actionModel = viewModel.action,
|
||||||
|
!dropdownField.isFirstResponder
|
||||||
setInitialValueFromPicker()
|
else { return }
|
||||||
|
MVMCoreUIActionHandler.performActionUnstructured(with: actionModel, sourceModel: viewModel, additionalData: additionalData, delegateObject: delegateObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc override func endInputing() {
|
private func updateValidation(_ isValid: Bool) {
|
||||||
super.endInputing()
|
let previousValidity = self.isValid
|
||||||
|
self.isValid = isValid
|
||||||
|
|
||||||
guard !pickerData.isEmpty else { return }
|
if previousValidity && !isValid {
|
||||||
|
showError = true
|
||||||
observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)])
|
} else if (!previousValidity && isValid) {
|
||||||
|
showError = false
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
|
||||||
super.set(with: model, delegateObject, additionalData)
|
|
||||||
|
|
||||||
guard let model = model as? ItemDropdownEntryFieldModel else { return }
|
|
||||||
|
|
||||||
pickerData = model.options
|
|
||||||
|
|
||||||
if let index = model.selectedIndex {
|
|
||||||
self.pickerView.selectRow(index, inComponent: 0, animated: false)
|
|
||||||
self.pickerView(pickerView, didSelectRow: index, inComponent: 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Picker Delegate
|
|
||||||
//--------------------------------------------------
|
|
||||||
|
|
||||||
@objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
|
||||||
|
|
||||||
@objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
|
||||||
pickerData.count
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
|
||||||
guard !pickerData.isEmpty else { return nil }
|
|
||||||
|
|
||||||
return pickerData[row]
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
|
||||||
guard !pickerData.isEmpty else { return }
|
|
||||||
|
|
||||||
itemDropdownEntryFieldModel?.selectedIndex = row
|
|
||||||
observeDropdownChange?(text, pickerData[row])
|
|
||||||
text = pickerData[row]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,16 +5,19 @@
|
|||||||
// Created by Kevin Christiano on 1/22/20.
|
// Created by Kevin Christiano on 1/22/20.
|
||||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||||
//
|
//
|
||||||
|
import VDS
|
||||||
|
|
||||||
@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel {
|
@objcMembers open class ItemDropdownEntryFieldModel: TextEntryFieldModel {
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
public override class var identifier: String { "dropDown" }
|
public override class var identifier: String { "dropDown" }
|
||||||
|
public var action: ActionModelProtocol?
|
||||||
public var options: [String] = []
|
public var options: [String] = []
|
||||||
public var selectedIndex: Int?
|
public var selectedIndex: Int?
|
||||||
|
public var showInlineLabel: Bool = false
|
||||||
|
public var feedbackTextPlacement: VDS.EntryFieldBase.HelperTextPlacement = .bottom
|
||||||
|
|
||||||
public init(with options: [String], selectedIndex: Int? = nil) {
|
public init(with options: [String], selectedIndex: Int? = nil) {
|
||||||
self.options = options
|
self.options = options
|
||||||
@ -42,6 +45,9 @@
|
|||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case options
|
case options
|
||||||
case selectedIndex
|
case selectedIndex
|
||||||
|
case action
|
||||||
|
case showInlineLabel
|
||||||
|
case feedbackTextPlacement
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -58,6 +64,9 @@
|
|||||||
self.selectedIndex = selectedIndex
|
self.selectedIndex = selectedIndex
|
||||||
baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil
|
baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil
|
||||||
}
|
}
|
||||||
|
showInlineLabel = try typeContainer.decodeIfPresent(Bool.self, forKey: .showInlineLabel) ?? false
|
||||||
|
feedbackTextPlacement = try typeContainer.decodeIfPresent(VDS.EntryFieldBase.HelperTextPlacement.self, forKey: .feedbackTextPlacement) ?? .bottom
|
||||||
|
action = try typeContainer.decodeModelIfPresent(codingKey: .action)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func encode(to encoder: Encoder) throws {
|
public override func encode(to encoder: Encoder) throws {
|
||||||
@ -65,5 +74,8 @@
|
|||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encode(options, forKey: .options)
|
try container.encode(options, forKey: .options)
|
||||||
try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex)
|
try container.encodeIfPresent(selectedIndex, forKey: .selectedIndex)
|
||||||
|
try container.encode(showInlineLabel, forKey: .showInlineLabel)
|
||||||
|
try container.encode(feedbackTextPlacement, forKey: .feedbackTextPlacement)
|
||||||
|
try container.encodeModelIfPresent(action, forKey: .action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -315,7 +315,9 @@ import UIKit
|
|||||||
self.showError = false
|
self.showError = false
|
||||||
}
|
}
|
||||||
self.isEnabled = model.enabled
|
self.isEnabled = model.enabled
|
||||||
|
if let text = model.text, !text.isEmpty {
|
||||||
self.text = model.text
|
self.text = model.text
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,347 @@
|
|||||||
|
//
|
||||||
|
// InputEntryField.swift
|
||||||
|
// MVMCoreUI
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 7/16/24.
|
||||||
|
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
@objcMembers open class InputEntryField: VDS.InputField, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
|
||||||
|
|
||||||
|
//------------------------------------------------------
|
||||||
|
// MARK: - Properties
|
||||||
|
//------------------------------------------------------
|
||||||
|
open var viewModel: TextEntryFieldModel!
|
||||||
|
open var delegateObject: MVMCoreUIDelegateObject?
|
||||||
|
open var additionalData: [AnyHashable : Any]?
|
||||||
|
|
||||||
|
// Form Validation
|
||||||
|
var fieldKey: String?
|
||||||
|
var fieldValue: JSONValue?
|
||||||
|
var groupName: String?
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Stored Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
public var isValid: Bool = true
|
||||||
|
|
||||||
|
/// Holds a reference to the delegating class so this class can internally influence the TextField behavior as well.
|
||||||
|
private weak var proprietorTextDelegate: UITextFieldDelegate?
|
||||||
|
|
||||||
|
private var isEditting: Bool = false {
|
||||||
|
didSet {
|
||||||
|
viewModel.selected = isEditting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Stored Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
private var observingForChange: Bool = false
|
||||||
|
|
||||||
|
/// Validate when user resigns editing. Default: true
|
||||||
|
open var validateWhenDoneEditing: Bool = true
|
||||||
|
|
||||||
|
open var shouldMaskWhileRecording: Bool {
|
||||||
|
return viewModel.shouldMaskRecordedView ?? false
|
||||||
|
}
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Computed Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
/// The text of this TextField.
|
||||||
|
open override var text: String? {
|
||||||
|
didSet {
|
||||||
|
viewModel?.text = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open override var errorText: String? {
|
||||||
|
get {
|
||||||
|
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||||
|
}
|
||||||
|
set {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placeholder access for the TextField.
|
||||||
|
public var placeholder: String? {
|
||||||
|
get { textField.placeholder }
|
||||||
|
set { textField.placeholder = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Delegate Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||||
|
public weak var observingTextFieldDelegate: ObservingTextFieldDelegate?
|
||||||
|
|
||||||
|
/// If you're using a ViewController, you must set this to it
|
||||||
|
open weak var uiTextFieldDelegate: UITextFieldDelegate?
|
||||||
|
{
|
||||||
|
get { textField.delegate }
|
||||||
|
set {
|
||||||
|
textField.delegate = self
|
||||||
|
proprietorTextDelegate = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Lifecycle
|
||||||
|
//--------------------------------------------------
|
||||||
|
open override func setup() {
|
||||||
|
super.setup()
|
||||||
|
//turn off internal required rule
|
||||||
|
useRequiredRule = false
|
||||||
|
|
||||||
|
publisher(for: .valueChanged)
|
||||||
|
.sink { [weak self] control in
|
||||||
|
guard let self else { return }
|
||||||
|
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||||
|
if (viewModel.type == .email) {
|
||||||
|
// remove spaces (either user entered Or auto-correct suggestion) for the email field
|
||||||
|
text = textField.text?.replacingOccurrences(of: " ", with: "")
|
||||||
|
}
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
|
textField
|
||||||
|
.publisher(for: .editingDidBegin)
|
||||||
|
.sink { [weak self] textView in
|
||||||
|
guard let self else { return }
|
||||||
|
isEditting = true
|
||||||
|
if viewModel.clearTextOnTap {
|
||||||
|
text = ""
|
||||||
|
}
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
|
textField
|
||||||
|
.publisher(for: .editingDidEnd)
|
||||||
|
.sink { [weak self] textView in
|
||||||
|
guard let self else { return }
|
||||||
|
isEditting = false
|
||||||
|
if validateWhenDoneEditing, let valid = viewModel.isValid {
|
||||||
|
updateValidation(valid)
|
||||||
|
}
|
||||||
|
regexTextFieldOutputIfAvailable()
|
||||||
|
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open override func updateView() {
|
||||||
|
super.updateView()
|
||||||
|
|
||||||
|
if let viewModel {
|
||||||
|
switch viewModel.type {
|
||||||
|
case .secure:
|
||||||
|
textField.isSecureTextEntry = true
|
||||||
|
textField.shouldMaskWhileRecording = true
|
||||||
|
|
||||||
|
case .numberSecure:
|
||||||
|
textField.isSecureTextEntry = true
|
||||||
|
textField.shouldMaskWhileRecording = true
|
||||||
|
textField.keyboardType = .numberPad
|
||||||
|
|
||||||
|
case .email:
|
||||||
|
textField.keyboardType = .emailAddress
|
||||||
|
|
||||||
|
case .securityCode, .creditCard, .password:
|
||||||
|
textField.shouldMaskWhileRecording = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override the preset keyboard set in type.
|
||||||
|
if let keyboardType = viewModel.assignKeyboardType() {
|
||||||
|
textField.keyboardType = keyboardType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func viewModelDidUpdate() {
|
||||||
|
|
||||||
|
fieldType = viewModel.type.toVDSFieldType()
|
||||||
|
text = viewModel.text
|
||||||
|
placeholder = viewModel.placeholder
|
||||||
|
|
||||||
|
labelText = viewModel.title
|
||||||
|
helperText = viewModel.feedback
|
||||||
|
isEnabled = viewModel.enabled
|
||||||
|
isReadOnly = viewModel.readOnly
|
||||||
|
isRequired = viewModel.required
|
||||||
|
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||||
|
width = viewModel.width
|
||||||
|
transparentBackground = viewModel.transparentBackground
|
||||||
|
|
||||||
|
containerView.accessibilityIdentifier = model.accessibilityIdentifier
|
||||||
|
textField.textAlignment = viewModel.textAlignment
|
||||||
|
textField.enableClipboardActions = viewModel.enableClipboardActions
|
||||||
|
textField.placeholder = viewModel.placeholder ?? ""
|
||||||
|
uiTextFieldDelegate = delegateObject?.uiTextFieldDelegate
|
||||||
|
observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate
|
||||||
|
|
||||||
|
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||||
|
|
||||||
|
viewModel.wasInitiallySelected = true
|
||||||
|
isEditting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.rules = rules
|
||||||
|
|
||||||
|
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||||
|
|
||||||
|
if isEditting {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
_ = self.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.updateUI = {
|
||||||
|
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if isEditting {
|
||||||
|
updateValidation(viewModel.isValid ?? true)
|
||||||
|
|
||||||
|
} else if viewModel.isValid ?? true && showError {
|
||||||
|
showError = false
|
||||||
|
}
|
||||||
|
isEnabled = viewModel.enabled
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.updateUIDynamicError = {
|
||||||
|
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
let validState = viewModel.isValid ?? false
|
||||||
|
if !validState && viewModel.shouldClearText {
|
||||||
|
text = ""
|
||||||
|
viewModel.shouldClearText = false
|
||||||
|
}
|
||||||
|
updateValidation(validState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Added to override text when view is reloaded.
|
||||||
|
if let text = viewModel.text, !text.isEmpty {
|
||||||
|
regexTextFieldOutputIfAvailable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Observing for Change (TextFieldDelegate)
|
||||||
|
//--------------------------------------------------
|
||||||
|
@objc public func setBothTextDelegates(to delegate: (UITextFieldDelegate & ObservingTextFieldDelegate)?) {
|
||||||
|
observingTextFieldDelegate = delegate
|
||||||
|
uiTextFieldDelegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
func regexTextFieldOutputIfAvailable() {
|
||||||
|
|
||||||
|
if let regex = viewModel?.displayFormat,
|
||||||
|
let mask = viewModel?.displayMask,
|
||||||
|
let finalText = text {
|
||||||
|
|
||||||
|
let range = NSRange(finalText.startIndex..., in: finalText)
|
||||||
|
|
||||||
|
if let regex = try? NSRegularExpression(pattern: regex) {
|
||||||
|
let maskedText = regex.stringByReplacingMatches(in: finalText,
|
||||||
|
range: range,
|
||||||
|
withTemplate: mask)
|
||||||
|
textField.text = maskedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func dismissFieldInput(_ sender: Any?) {
|
||||||
|
_ = resignFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateValidation(_ isValid: Bool) {
|
||||||
|
let previousValidity = self.isValid
|
||||||
|
self.isValid = isValid
|
||||||
|
|
||||||
|
if previousValidity && !isValid {
|
||||||
|
showError = true
|
||||||
|
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||||
|
} else if (!previousValidity && isValid) {
|
||||||
|
showError = false
|
||||||
|
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - MoleculeViewProtocol
|
||||||
|
//--------------------------------------------------
|
||||||
|
@objc open func updateView(_ size: CGFloat) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension InputEntryField {
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Implemented TextField Delegate
|
||||||
|
//--------------------------------------------------
|
||||||
|
@discardableResult
|
||||||
|
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||||
|
??
|
||||||
|
super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
|
proprietorTextDelegate?.textFieldDidBeginEditing?(textField) ?? super.textFieldDidBeginEditing(textField)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public override func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
|
proprietorTextDelegate?.textFieldDidEndEditing?(textField) ?? super.textFieldDidEndEditing(textField)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||||
|
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||||
|
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||||
|
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Accessibility
|
||||||
|
extension InputEntryField {
|
||||||
|
|
||||||
|
@objc open func pushAccessibilityNotification() {
|
||||||
|
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
UIAccessibility.post(notification: .layoutChanged, argument: containerView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct ViewMasking {
|
||||||
|
static var shouldMaskWhileRecording: UInt8 = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VDS.TextField: ViewMaskingProtocol {
|
||||||
|
public var shouldMaskWhileRecording: Bool {
|
||||||
|
get {
|
||||||
|
return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ import MVMCore
|
|||||||
/**
|
/**
|
||||||
This class provides the convenience of formatting the MDN entered/displayer for the user.
|
This class provides the convenience of formatting the MDN entered/displayer for the user.
|
||||||
*/
|
*/
|
||||||
@objcMembers open class MdnEntryField: TextEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate {
|
@objcMembers open class MdnEntryField: InputEntryField, ABPeoplePickerNavigationControllerDelegate, CNContactPickerDelegate {
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Stored Properties
|
// MARK: - Stored Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -48,51 +48,16 @@ import MVMCore
|
|||||||
set { text = MVMCoreUIUtility.formatMdn(newValue) }
|
set { text = MVMCoreUIUtility.formatMdn(newValue) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggles selected or original (unselected) UI.
|
|
||||||
public override var isSelected: Bool {
|
|
||||||
get { return entryFieldContainer.isSelected }
|
|
||||||
set (selected) {
|
|
||||||
if selected && showError {
|
|
||||||
showError = false
|
|
||||||
}
|
|
||||||
|
|
||||||
super.isSelected = selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Initializers
|
|
||||||
//--------------------------------------------------
|
|
||||||
|
|
||||||
@objc public override init(frame: CGRect) {
|
|
||||||
super.init(frame: .zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public convenience init() {
|
|
||||||
self.init(frame: .zero)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc required public init?(coder: NSCoder) {
|
|
||||||
super.init(coder: coder)
|
|
||||||
fatalError("MdnEntryField xib not supported.")
|
|
||||||
}
|
|
||||||
|
|
||||||
required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
|
||||||
super.init(model: model, delegateObject, additionalData)
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Setup
|
// MARK: - Setup
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
@objc public override func setupFieldContainerContent(_ container: UIView) {
|
open override func setup() {
|
||||||
super.setupFieldContainerContent(container)
|
super.setup()
|
||||||
|
setupTextFieldToolbar()
|
||||||
textField.keyboardType = .numberPad
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func setupTextFieldToolbar() {
|
open func setupTextFieldToolbar() {
|
||||||
|
|
||||||
let toolbar = UIToolbar.createEmptyToolbar()
|
let toolbar = UIToolbar.createEmptyToolbar()
|
||||||
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||||
let contacts = UIBarButtonItem(title: MVMCoreUIUtility.hardcodedString(withKey: "textfield_contacts_barbutton"), style: .plain, target: self, action: #selector(getContacts))
|
let contacts = UIBarButtonItem(title: MVMCoreUIUtility.hardcodedString(withKey: "textfield_contacts_barbutton"), style: .plain, target: self, action: #selector(getContacts))
|
||||||
@ -104,39 +69,6 @@ import MVMCore
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Methods
|
// MARK: - Methods
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
@objc public func hasValidMDN() -> Bool {
|
|
||||||
|
|
||||||
guard let MDN = mdn, !MDN.isEmpty else { return false }
|
|
||||||
|
|
||||||
if isNationalMDN {
|
|
||||||
return MVMCoreUIUtility.validateMDNString(MDN)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MVMCoreUIUtility.validateInternationalMDNString(MDN)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func validateMDNTextField() -> Bool {
|
|
||||||
|
|
||||||
guard !shouldValidateMDN, let MDN = mdn, !MDN.isEmpty else {
|
|
||||||
isValid = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
isValid = hasValidMDN()
|
|
||||||
|
|
||||||
if self.isValid {
|
|
||||||
showError = false
|
|
||||||
|
|
||||||
} else {
|
|
||||||
entryFieldModel?.errorMessage = entryFieldModel?.errorMessage ?? MVMCoreUIUtility.hardcodedString(withKey: "textfield_phone_format_error_message")
|
|
||||||
showError = true
|
|
||||||
UIAccessibility.post(notification: .layoutChanged, argument: textField)
|
|
||||||
}
|
|
||||||
|
|
||||||
return isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc public func getContacts(_ sender: Any?) {
|
@objc public func getContacts(_ sender: Any?) {
|
||||||
|
|
||||||
let picker = CNContactPickerViewController()
|
let picker = CNContactPickerViewController()
|
||||||
@ -152,11 +84,12 @@ import MVMCore
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - MoleculeViewProtocol
|
// MARK: - MoleculeViewProtocol
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
public override func viewModelDidUpdate() {
|
||||||
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
viewModel.type = .phone
|
||||||
super.set(with: model, delegateObject, additionalData)
|
super.viewModelDidUpdate()
|
||||||
|
if let phoneNumber = viewModel.text {
|
||||||
textField.keyboardType = .phonePad
|
text = phoneNumber.formatUSNumber()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -179,7 +112,6 @@ import MVMCore
|
|||||||
let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1)
|
let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1)
|
||||||
unformattedMDN = String(unformedMDN[startIndex...])
|
unformattedMDN = String(unformedMDN[startIndex...])
|
||||||
}
|
}
|
||||||
|
|
||||||
text = unformattedMDN
|
text = unformattedMDN
|
||||||
textFieldShouldReturn(textField)
|
textFieldShouldReturn(textField)
|
||||||
textFieldDidEndEditing(textField)
|
textFieldDidEndEditing(textField)
|
||||||
@ -191,50 +123,36 @@ import MVMCore
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
@objc public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
@objc public override func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||||
|
_ = resignFirstResponder()
|
||||||
textField.resignFirstResponder()
|
let superValue = super.textFieldShouldReturn(textField)
|
||||||
|
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? superValue
|
||||||
return proprietorTextDelegate?.textFieldShouldReturn?(textField) ?? true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
@objc public override func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||||
|
let superValue = super.textField(textField, shouldChangeCharactersIn: range, replacementString: string)
|
||||||
if !MVMCoreUIUtility.validate(string, withRegularExpression: RegularExpressionDigitOnly) {
|
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? superValue
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return proprietorTextDelegate?.textField?(textField, shouldChangeCharactersIn: range, replacementString: string) ?? true
|
@objc public override func textFieldDidBeginEditing(_ textField: UITextField) {
|
||||||
}
|
super.textFieldDidBeginEditing(textField)
|
||||||
|
|
||||||
@objc public func textFieldDidBeginEditing(_ textField: UITextField) {
|
|
||||||
|
|
||||||
textField.text = MVMCoreUIUtility.removeMdnFormat(textField.text)
|
|
||||||
proprietorTextDelegate?.textFieldDidBeginEditing?(textField)
|
proprietorTextDelegate?.textFieldDidBeginEditing?(textField)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func textFieldDidEndEditing(_ textField: UITextField) {
|
@objc public override func textFieldDidEndEditing(_ textField: UITextField) {
|
||||||
|
|
||||||
proprietorTextDelegate?.textFieldDidEndEditing?(textField)
|
proprietorTextDelegate?.textFieldDidEndEditing?(textField)
|
||||||
|
super.textFieldDidEndEditing(textField)
|
||||||
if validateMDNTextField() {
|
|
||||||
if isNationalMDN {
|
|
||||||
textField.text = MVMCoreUIUtility.formatMdn(textField.text)
|
|
||||||
}
|
|
||||||
// Validate the base input field along with triggering form field validation rules.
|
|
||||||
validateText()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
@objc public override func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||||
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
@objc public override func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||||
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
@objc public override func textFieldShouldClear(_ textField: UITextField) -> Bool {
|
||||||
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,4 +12,9 @@
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
public override class var identifier: String { "mdnEntryField" }
|
public override class var identifier: String { "mdnEntryField" }
|
||||||
|
|
||||||
|
open override func formFieldServerValue() -> AnyHashable? {
|
||||||
|
guard let value = formFieldValue() as? String else { return nil }
|
||||||
|
return value.filter { $0.isNumber }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,9 +11,9 @@ import UIKit
|
|||||||
|
|
||||||
@objc public protocol ObservingTextFieldDelegate {
|
@objc public protocol ObservingTextFieldDelegate {
|
||||||
/// Called when the entered text becomes valid based on the validation block
|
/// Called when the entered text becomes valid based on the validation block
|
||||||
@objc optional func isValid(textfield: TextEntryField?)
|
@objc optional func isValid(textfield: Any?)
|
||||||
/// Called when the entered text becomes invalid based on the validation block
|
/// Called when the entered text becomes invalid based on the validation block
|
||||||
@objc optional func isInvalid(textfield: TextEntryField?)
|
@objc optional func isInvalid(textfield: Any?)
|
||||||
/// Dismisses the keyboard.
|
/// Dismisses the keyboard.
|
||||||
@objc optional func dismissFieldInput(_ sender: Any?)
|
@objc optional func dismissFieldInput(_ sender: Any?)
|
||||||
}
|
}
|
||||||
@ -317,9 +317,9 @@ import UIKit
|
|||||||
super.shouldShowError(showError)
|
super.shouldShowError(showError)
|
||||||
|
|
||||||
if showError {
|
if showError {
|
||||||
observingTextFieldDelegate?.isValid?(textfield: self)
|
|
||||||
} else {
|
|
||||||
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
observingTextFieldDelegate?.isInvalid?(textfield: self)
|
||||||
|
} else {
|
||||||
|
observingTextFieldDelegate?.isValid?(textfield: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,9 +5,10 @@
|
|||||||
// Created by Kevin Christiano on 1/22/20.
|
// Created by Kevin Christiano on 1/22/20.
|
||||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||||
//
|
//
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
@objcMembers open class TextEntryFieldModel: EntryFieldModel, FormFieldInternalValidatableProtocol {
|
||||||
|
|
||||||
@objcMembers open class TextEntryFieldModel: EntryFieldModel {
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Types
|
// MARK: - Types
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -20,6 +21,39 @@
|
|||||||
case email
|
case email
|
||||||
case text
|
case text
|
||||||
case phone
|
case phone
|
||||||
|
|
||||||
|
//additional
|
||||||
|
case inlineAction
|
||||||
|
case creditCard
|
||||||
|
case date
|
||||||
|
case securityCode
|
||||||
|
|
||||||
|
public func toVDSFieldType() -> VDS.InputField.FieldType {
|
||||||
|
switch self {
|
||||||
|
case .password:
|
||||||
|
.password
|
||||||
|
case .secure:
|
||||||
|
.text
|
||||||
|
case .number:
|
||||||
|
.number
|
||||||
|
case .numberSecure:
|
||||||
|
.number
|
||||||
|
case .email:
|
||||||
|
.text
|
||||||
|
case .text:
|
||||||
|
.text
|
||||||
|
case .phone:
|
||||||
|
.telephone
|
||||||
|
case .inlineAction:
|
||||||
|
.inlineAction
|
||||||
|
case .creditCard:
|
||||||
|
.creditCard
|
||||||
|
case .date:
|
||||||
|
.date
|
||||||
|
case .securityCode:
|
||||||
|
.securityCode
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -33,12 +67,21 @@
|
|||||||
public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
||||||
public var textAlignment: NSTextAlignment = .left
|
public var textAlignment: NSTextAlignment = .left
|
||||||
public var keyboardOverride: String?
|
public var keyboardOverride: String?
|
||||||
public var type: EntryType?
|
public var type: EntryType = .text
|
||||||
public var clearTextOnTap: Bool = false
|
public var clearTextOnTap: Bool = false
|
||||||
public var displayFormat: String?
|
public var displayFormat: String?
|
||||||
public var displayMask: String?
|
public var displayMask: String?
|
||||||
public var enableClipboardActions: Bool = true
|
public var enableClipboardActions: Bool = true
|
||||||
|
|
||||||
|
public var tooltip: TooltipModel?
|
||||||
|
public var transparentBackground: Bool = false
|
||||||
|
public var width: CGFloat?
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - FormFieldInternalValidatableProtocol
|
||||||
|
//--------------------------------------------------
|
||||||
|
open var rules: [AnyRule<String>]?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Initializers
|
// MARK: - Initializers
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -114,6 +157,9 @@
|
|||||||
case displayFormat
|
case displayFormat
|
||||||
case displayMask
|
case displayMask
|
||||||
case enableClipboardActions
|
case enableClipboardActions
|
||||||
|
case tooltip
|
||||||
|
case transparentBackground
|
||||||
|
case width
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -128,7 +174,7 @@
|
|||||||
displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat)
|
displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat)
|
||||||
keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride)
|
keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride)
|
||||||
displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask)
|
displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask)
|
||||||
type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type)
|
type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) ?? .text
|
||||||
|
|
||||||
if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) {
|
if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) {
|
||||||
self.clearTextOnTap = clearTextOnTap
|
self.clearTextOnTap = clearTextOnTap
|
||||||
@ -149,6 +195,10 @@
|
|||||||
if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) {
|
if let enableClipboardActions = try typeContainer.decodeIfPresent(Bool.self, forKey: .enableClipboardActions) {
|
||||||
self.enableClipboardActions = enableClipboardActions
|
self.enableClipboardActions = enableClipboardActions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tooltip = try typeContainer.decodeIfPresent(TooltipModel.self, forKey: .tooltip)
|
||||||
|
transparentBackground = try typeContainer.decodeIfPresent(Bool.self, forKey: .transparentBackground) ?? false
|
||||||
|
width = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .width)
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func encode(to encoder: Encoder) throws {
|
open override func encode(to encoder: Encoder) throws {
|
||||||
@ -164,5 +214,8 @@
|
|||||||
try container.encode(disabledTextColor, forKey: .disabledTextColor)
|
try container.encode(disabledTextColor, forKey: .disabledTextColor)
|
||||||
try container.encode(clearTextOnTap, forKey: .clearTextOnTap)
|
try container.encode(clearTextOnTap, forKey: .clearTextOnTap)
|
||||||
try container.encode(enableClipboardActions, forKey: .enableClipboardActions)
|
try container.encode(enableClipboardActions, forKey: .enableClipboardActions)
|
||||||
|
try container.encodeIfPresent(tooltip, forKey: .tooltip)
|
||||||
|
try container.encode(transparentBackground, forKey: .transparentBackground)
|
||||||
|
try container.encodeIfPresent(width, forKey: .width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,100 +7,60 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
open class TextViewEntryField: VDS.TextArea, VDSMoleculeViewProtocol, ObservingTextFieldDelegate, ViewMaskingProtocol {
|
||||||
class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDelegate {
|
//------------------------------------------------------
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Outlets
|
|
||||||
//--------------------------------------------------
|
|
||||||
|
|
||||||
open private(set) var textView: TextView = {
|
|
||||||
let textView = TextView()
|
|
||||||
textView.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
||||||
return textView
|
|
||||||
}()
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
//--------------------------------------------------
|
//------------------------------------------------------
|
||||||
|
open var viewModel: TextViewEntryFieldModel!
|
||||||
|
open var delegateObject: MVMCoreUIDelegateObject?
|
||||||
|
open var additionalData: [AnyHashable : Any]?
|
||||||
|
|
||||||
private var observingForChange: Bool = false
|
// Form Validation
|
||||||
|
open var fieldKey: String?
|
||||||
|
open var fieldValue: JSONValue?
|
||||||
|
open var groupName: String?
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Stored Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
public var isValid: Bool = true
|
||||||
|
|
||||||
|
private var isEditting: Bool = false {
|
||||||
|
didSet {
|
||||||
|
viewModel.selected = isEditting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open var shouldMaskWhileRecording: Bool {
|
||||||
public var textViewEntryFieldModel: TextViewEntryFieldModel? {
|
return viewModel.shouldMaskRecordedView ?? false
|
||||||
model as? TextViewEntryFieldModel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var isEnabled: Bool {
|
/// Placeholder access for the textView.
|
||||||
get { super.isEnabled }
|
open var placeholder: String? {
|
||||||
set (enabled) {
|
get { viewModel?.placeholder }
|
||||||
super.isEnabled = enabled
|
set {
|
||||||
|
textView.placeholder = newValue ?? ""
|
||||||
DispatchQueue.main.async { [weak self] in
|
viewModel?.placeholder = newValue
|
||||||
guard let self = self else { return }
|
|
||||||
|
|
||||||
self.textView.isEnabled = enabled
|
|
||||||
if self.textView.isShowingPlaceholder {
|
|
||||||
self.textView.textColor = self.textView.placeholderTextColor
|
|
||||||
} else {
|
|
||||||
self.textView.textColor = (self.textView.isEnabled ? self.textViewEntryFieldModel?.enabledTextColor : self.textViewEntryFieldModel?.disabledTextColor)?.uiColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var showError: Bool {
|
|
||||||
get { super.showError }
|
|
||||||
set (error) {
|
|
||||||
|
|
||||||
if error {
|
|
||||||
textView.accessibilityValue = String(format: MVMCoreUIUtility.hardcodedString(withKey: "textView_error_message") ?? "", textView.text ?? "", entryFieldModel?.errorMessage ?? "")
|
|
||||||
} else {
|
|
||||||
textView.accessibilityValue = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
super.showError = error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The text of this textView.
|
/// The text of this textView.
|
||||||
open override var text: String? {
|
open override var text: String? {
|
||||||
get { textViewEntryFieldModel?.text }
|
didSet {
|
||||||
set {
|
viewModel?.text = text
|
||||||
textView.text = newValue
|
|
||||||
textViewEntryFieldModel?.text = newValue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Placeholder access for the textView.
|
open override var errorText: String? {
|
||||||
public var placeholder: String? {
|
get {
|
||||||
get { textViewEntryFieldModel?.placeholder }
|
viewModel.dynamicErrorMessage ?? viewModel.errorMessage
|
||||||
set {
|
|
||||||
textView.placeholder = newValue ?? ""
|
|
||||||
textViewEntryFieldModel?.placeholder = newValue
|
|
||||||
textView.setPlaceholderIfAvailable()
|
|
||||||
}
|
}
|
||||||
}
|
set {}
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - Constraint
|
|
||||||
//--------------------------------------------------
|
|
||||||
|
|
||||||
public var heightConstraint: NSLayoutConstraint?
|
|
||||||
private var topConstraint: NSLayoutConstraint?
|
|
||||||
private var leadingConstraint: NSLayoutConstraint?
|
|
||||||
private var trailingConstraint: NSLayoutConstraint?
|
|
||||||
private var bottomConstraint: NSLayoutConstraint?
|
|
||||||
|
|
||||||
private func adjustMarginConstraints(constant: CGFloat) {
|
|
||||||
|
|
||||||
topConstraint?.constant = constant
|
|
||||||
leadingConstraint?.constant = constant
|
|
||||||
trailingConstraint?.constant = constant
|
|
||||||
bottomConstraint?.constant = constant
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -108,198 +68,178 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele
|
|||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
|
||||||
/// The delegate and block for validation. Validates if the text that the user has entered.
|
/// The delegate and block for validation. Validates if the text that the user has entered.
|
||||||
public weak var observingTextViewDelegate: ObservingTextFieldDelegate? {
|
open weak var observingTextViewDelegate: ObservingTextFieldDelegate?
|
||||||
didSet {
|
|
||||||
if observingTextViewDelegate != nil && !observingForChange {
|
|
||||||
observingForChange = true
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(valueChanged), name: UITextView.textDidChangeNotification, object: textView)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(endInputing), name: UITextView.textDidEndEditingNotification, object: textView)
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(startEditing), name: UITextView.textDidBeginEditingNotification, object: textView)
|
|
||||||
|
|
||||||
} else if observingTextViewDelegate == nil && observingForChange {
|
|
||||||
observingForChange = false
|
|
||||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidChangeNotification, object: textView)
|
|
||||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidEndEditingNotification, object: textView)
|
|
||||||
NotificationCenter.default.removeObserver(self, name: UITextView.textDidBeginEditingNotification, object: textView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If you're using a ViewController, you must set this to it
|
/// If you're using a ViewController, you must set this to it
|
||||||
public weak var uiTextViewDelegate: UITextViewDelegate? {
|
open weak var uiTextViewDelegate: UITextViewDelegate? {
|
||||||
get { textView.delegate }
|
get { textView.delegate }
|
||||||
set { textView.delegate = newValue }
|
set { textView.delegate = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
|
@objc open func setBothTextDelegates(to delegate: (UITextViewDelegate & ObservingTextFieldDelegate)?) {
|
||||||
observingTextViewDelegate = delegate
|
observingTextViewDelegate = delegate
|
||||||
uiTextViewDelegate = delegate
|
uiTextViewDelegate = delegate
|
||||||
}
|
}
|
||||||
|
|
||||||
open func setupTextViewToolbar() {
|
|
||||||
let observingDelegate = observingTextViewDelegate ?? self
|
|
||||||
textView.inputAccessoryView = UIToolbar.getToolbarWithDoneButton(delegate: observingDelegate,
|
|
||||||
action: #selector(observingDelegate.dismissFieldInput))
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
|
open override func setup() {
|
||||||
|
super.setup()
|
||||||
|
//turn off internal required rule
|
||||||
|
useRequiredRule = false
|
||||||
|
|
||||||
@objc open override func setupFieldContainerContent(_ container: UIView) {
|
publisher(for: .valueChanged)
|
||||||
|
.sink { [weak self] control in
|
||||||
|
guard let self else { return }
|
||||||
|
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
|
||||||
|
}.store(in: &subscribers)
|
||||||
|
|
||||||
container.addSubview(textView)
|
textView
|
||||||
|
.publisher(for: .editingDidBegin)
|
||||||
|
.sink { [weak self] textView in
|
||||||
|
guard let self else { return }
|
||||||
|
isEditting = true
|
||||||
|
|
||||||
topConstraint = textView.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three)
|
}.store(in: &subscribers)
|
||||||
leadingConstraint = textView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three)
|
|
||||||
trailingConstraint = container.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: Padding.Three)
|
|
||||||
bottomConstraint = container.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: Padding.Three)
|
|
||||||
|
|
||||||
topConstraint?.isActive = true
|
textView
|
||||||
leadingConstraint?.isActive = true
|
.publisher(for: .editingDidEnd)
|
||||||
trailingConstraint?.isActive = true
|
.sink { [weak self] textView in
|
||||||
bottomConstraint?.isActive = true
|
guard let self else { return }
|
||||||
|
isEditting = false
|
||||||
heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0)
|
if let valid = viewModel.isValid {
|
||||||
accessibilityElements = [textView]
|
updateValidation(valid)
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func updateView(_ size: CGFloat) {
|
}.store(in: &subscribers)
|
||||||
super.updateView(size)
|
|
||||||
textView.updateView(size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func reset() {
|
open func viewModelDidUpdate() {
|
||||||
super.reset()
|
|
||||||
|
|
||||||
textView.reset()
|
text = viewModel.text
|
||||||
adjustMarginConstraints(constant: Padding.Three)
|
minHeight = viewModel.minHeight
|
||||||
heightConstraint?.constant = 0
|
maxLength = viewModel.maxLength
|
||||||
heightConstraint?.isActive = false
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
labelText = viewModel.title
|
||||||
// MARK: - Methods
|
helperText = viewModel.feedback
|
||||||
//--------------------------------------------------
|
isEnabled = viewModel.enabled
|
||||||
|
isReadOnly = viewModel.readOnly
|
||||||
|
isRequired = viewModel.required
|
||||||
|
tooltipModel = viewModel.tooltip?.toVDSTooltipModel()
|
||||||
|
width = viewModel.width
|
||||||
|
transparentBackground = viewModel.transparentBackground
|
||||||
|
|
||||||
/// Validates the text of the entry field.
|
|
||||||
@objc public override func validateText() {
|
|
||||||
text = textView.text
|
|
||||||
super.validateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executes on UITextView.textDidBeginEditingNotification
|
|
||||||
@objc override func startEditing() {
|
|
||||||
super.startEditing()
|
|
||||||
_ = textView.becomeFirstResponder()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executes on UITextView.textDidChangeNotification (each character entry)
|
|
||||||
@objc override func valueChanged() {
|
|
||||||
super.valueChanged()
|
|
||||||
validateText()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Executes on UITextView.textDidEndEditingNotification
|
|
||||||
@objc override func endInputing() {
|
|
||||||
super.endInputing()
|
|
||||||
|
|
||||||
// Don't show error till user starts typing.
|
|
||||||
guard text?.count ?? 0 != 0 else {
|
|
||||||
showError = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let isValid = textViewEntryFieldModel?.isValid {
|
|
||||||
self.isValid = isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
showError = !isValid
|
|
||||||
}
|
|
||||||
|
|
||||||
//--------------------------------------------------
|
|
||||||
// MARK: - MoleculeViewProtocol
|
|
||||||
//--------------------------------------------------
|
|
||||||
|
|
||||||
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
|
||||||
super.set(with: model, delegateObject, additionalData)
|
|
||||||
|
|
||||||
guard let model = model as? TextViewEntryFieldModel else { return }
|
|
||||||
|
|
||||||
if let height = model.height {
|
|
||||||
heightConstraint?.constant = height
|
|
||||||
heightConstraint?.isActive = true
|
|
||||||
}
|
|
||||||
|
|
||||||
text = model.text
|
|
||||||
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
uiTextViewDelegate = delegateObject?.uiTextViewDelegate
|
||||||
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
|
observingTextViewDelegate = delegateObject?.observingTextFieldDelegate
|
||||||
|
|
||||||
if let accessibilityText = model.accessibilityText {
|
if let accessibilityText = viewModel.accessibilityText {
|
||||||
accessibilityLabel = accessibilityText
|
accessibilityLabel = accessibilityText
|
||||||
}
|
}
|
||||||
|
containerView.accessibilityIdentifier = viewModel.accessibilityIdentifier
|
||||||
|
textView.isEditable = viewModel.editable
|
||||||
|
textView.textAlignment = viewModel.textAlignment
|
||||||
|
textView.placeholder = viewModel.placeholder ?? ""
|
||||||
|
|
||||||
textView.isEditable = model.editable
|
if (viewModel.selected ?? false) && !viewModel.wasInitiallySelected {
|
||||||
textView.textAlignment = model.textAlignment
|
|
||||||
textView.accessibilityIdentifier = model.accessibilityIdentifier
|
|
||||||
textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor
|
|
||||||
textView.font = model.fontStyle.getFont()
|
|
||||||
textView.placeholder = model.placeholder ?? ""
|
|
||||||
textView.placeholderFontStyle = model.placeholderFontStyle
|
|
||||||
textView.placeholderTextColor = model.placeholderTextColor.uiColor
|
|
||||||
textView.setPlaceholderIfAvailable()
|
|
||||||
|
|
||||||
switch model.type {
|
viewModel.wasInitiallySelected = true
|
||||||
case .secure, .password:
|
isEditting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch viewModel.type {
|
||||||
|
case .secure:
|
||||||
textView.isSecureTextEntry = true
|
textView.isSecureTextEntry = true
|
||||||
|
textView.shouldMaskWhileRecording = true
|
||||||
|
|
||||||
case .numberSecure:
|
case .numberSecure:
|
||||||
textView.isSecureTextEntry = true
|
textView.isSecureTextEntry = true
|
||||||
textView.keyboardType = .numberPad
|
textView.shouldMaskWhileRecording = true
|
||||||
|
|
||||||
case .number:
|
|
||||||
textView.keyboardType = .numberPad
|
textView.keyboardType = .numberPad
|
||||||
|
|
||||||
case .email:
|
case .email:
|
||||||
textView.keyboardType = .emailAddress
|
textView.keyboardType = .emailAddress
|
||||||
|
|
||||||
default: break
|
case .securityCode, .creditCard, .password:
|
||||||
|
textView.shouldMaskWhileRecording = true
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override the preset keyboard set in type.
|
||||||
|
if let keyboardType = viewModel.assignKeyboardType() {
|
||||||
|
textView.keyboardType = keyboardType
|
||||||
|
}
|
||||||
|
|
||||||
|
/// append any internal rules:
|
||||||
|
viewModel.rules = rules
|
||||||
|
|
||||||
/// No point in configuring if the TextView is Read-only.
|
/// No point in configuring if the TextView is Read-only.
|
||||||
if textView.isEditable {
|
if textView.isEditable {
|
||||||
FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate)
|
FormValidator.setupValidation(for: viewModel, delegate: delegateObject?.formHolderDelegate)
|
||||||
setupTextViewToolbar()
|
|
||||||
|
|
||||||
if isSelected {
|
if isEditting {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
_ = self.textView.becomeFirstResponder()
|
_ = self.becomeFirstResponder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if model.hideBorders {
|
viewModel.updateUI = {
|
||||||
adjustMarginConstraints(constant: 0)
|
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
if isEditting {
|
||||||
|
updateValidation(viewModel.isValid ?? true)
|
||||||
|
|
||||||
|
} else if viewModel.isValid ?? true && showError {
|
||||||
|
showError = false
|
||||||
}
|
}
|
||||||
updateAccessibility(model: model)
|
isEnabled = viewModel.enabled
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateAccessibility(model: TextViewEntryFieldModel) {
|
viewModel.updateUIDynamicError = {
|
||||||
|
MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
var message = ""
|
let validState = viewModel.isValid ?? false
|
||||||
|
if !validState && viewModel.shouldClearText {
|
||||||
if let titleText = model.accessibilityText ?? model.title {
|
text = ""
|
||||||
message += "\(titleText) \( model.enabled ? String(format: (MVMCoreUIUtility.hardcodedString(withKey: "textfield_optional")) ?? "") : "" ) \(self.textView.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")"
|
viewModel.shouldClearText = false
|
||||||
|
}
|
||||||
|
updateValidation(validState)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if let feedback = model.feedback {
|
|
||||||
message += ", " + feedback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let errorMessage = errorLabel.text {
|
private func updateValidation(_ isValid: Bool) {
|
||||||
message += ", " + errorMessage
|
let previousValidity = self.isValid
|
||||||
|
self.isValid = isValid
|
||||||
|
|
||||||
|
if previousValidity && !isValid {
|
||||||
|
showError = true
|
||||||
|
} else if (!previousValidity && isValid) {
|
||||||
|
showError = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
textView.accessibilityLabel = message
|
//--------------------------------------------------
|
||||||
|
// MARK: - MoleculeViewProtocol
|
||||||
|
//--------------------------------------------------
|
||||||
|
open func updateView(_ size: CGFloat) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VDS.TextView: ViewMaskingProtocol {
|
||||||
|
public var shouldMaskWhileRecording: Bool {
|
||||||
|
get {
|
||||||
|
return (objc_getAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording) as? Bool) ?? false
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
objc_setAssociatedObject(self, &ViewMasking.shouldMaskWhileRecording, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,9 +7,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
public class TextViewEntryFieldModel: TextEntryFieldModel {
|
||||||
class TextViewEntryFieldModel: TextEntryFieldModel {
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -17,12 +17,10 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
|||||||
public override class var identifier: String { "textView" }
|
public override class var identifier: String { "textView" }
|
||||||
|
|
||||||
public var accessibilityText: String?
|
public var accessibilityText: String?
|
||||||
public var fontStyle: Styler.Font = Styler.Font.RegularBodyLarge
|
|
||||||
public var height: CGFloat?
|
|
||||||
public var placeholderTextColor: Color = Color(uiColor: .mvmCoolGray3)
|
|
||||||
public var placeholderFontStyle: Styler.Font = Styler.Font.RegularMicro
|
|
||||||
public var editable: Bool = true
|
public var editable: Bool = true
|
||||||
public var showsPlaceholder: Bool = false
|
public var showsPlaceholder: Bool = false
|
||||||
|
public var minHeight: VDS.TextArea.Height = .twoX
|
||||||
|
public var maxLength: Int?
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
// MARK: - Keys
|
// MARK: - Keys
|
||||||
@ -30,11 +28,9 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
|||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case accessibilityText
|
case accessibilityText
|
||||||
case fontStyle
|
|
||||||
case height
|
|
||||||
case placeholderFontStyle
|
|
||||||
case placeholderTextColor
|
|
||||||
case editable
|
case editable
|
||||||
|
case minHeight
|
||||||
|
case maxLength
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -45,34 +41,18 @@ class TextViewEntryFieldModel: TextEntryFieldModel {
|
|||||||
try super.init(from: decoder)
|
try super.init(from: decoder)
|
||||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
if let placeholderFontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .placeholderFontStyle) {
|
editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) ?? true
|
||||||
self.placeholderFontStyle = placeholderFontStyle
|
minHeight = try typeContainer.decodeIfPresent(VDS.TextArea.Height.self, forKey: .minHeight) ?? .twoX
|
||||||
}
|
maxLength = try typeContainer.decodeIfPresent(Int.self, forKey: .maxLength)
|
||||||
|
|
||||||
if let placeholderTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .placeholderTextColor) {
|
|
||||||
self.placeholderTextColor = placeholderTextColor
|
|
||||||
}
|
|
||||||
|
|
||||||
if let fontStyle = try typeContainer.decodeIfPresent(Styler.Font.self, forKey: .fontStyle) {
|
|
||||||
self.fontStyle = fontStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
if let editable = try typeContainer.decodeIfPresent(Bool.self, forKey: .editable) {
|
|
||||||
self.editable = editable
|
|
||||||
}
|
|
||||||
|
|
||||||
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText)
|
||||||
height = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func encode(to encoder: Encoder) throws {
|
public override func encode(to encoder: Encoder) throws {
|
||||||
try super.encode(to: encoder)
|
try super.encode(to: encoder)
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText)
|
||||||
try container.encodeIfPresent(height, forKey: .height)
|
|
||||||
try container.encode(fontStyle, forKey: .fontStyle)
|
|
||||||
try container.encode(editable, forKey: .editable)
|
try container.encode(editable, forKey: .editable)
|
||||||
try container.encode(placeholderFontStyle, forKey: .placeholderFontStyle)
|
try container.encode(minHeight, forKey: .minHeight)
|
||||||
try container.encode(placeholderTextColor, forKey: .placeholderTextColor)
|
try container.encodeIfPresent(maxLength, forKey: .maxLength)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,4 +53,15 @@ open class BadgeModel: MoleculeModelProtocol {
|
|||||||
try container.encode(numberOfLines, forKey: .numberOfLines)
|
try container.encode(numberOfLines, forKey: .numberOfLines)
|
||||||
try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
|
try container.encodeIfPresent(maxWidth, forKey: .maxWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func isEqual(to model: any ModelComparisonProtocol) -> Bool {
|
||||||
|
guard let model = model as? BadgeModel else { return false }
|
||||||
|
return self.backgroundColor == model.backgroundColor
|
||||||
|
&& self.fillColor == model.fillColor
|
||||||
|
&& self.numberOfLines == model.numberOfLines
|
||||||
|
&& self.text == model.text
|
||||||
|
&& self.surface == model.surface
|
||||||
|
&& self.accessibilityText == model.accessibilityText
|
||||||
|
&& self.maxWidth == model.maxWidth
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
128
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift
Normal file
128
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBar.swift
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
//
|
||||||
|
// CircularProgressBar.swift
|
||||||
|
// MVMCoreUI
|
||||||
|
//
|
||||||
|
// Created by Xi Zhang on 7/5/24.
|
||||||
|
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@objcMembers open class CircularProgressBar: View, MVMCoreUIViewConstrainingProtocol {
|
||||||
|
|
||||||
|
var heightConstraint: NSLayoutConstraint?
|
||||||
|
var graphModel: CircularProgressBarModel? {
|
||||||
|
return model as? CircularProgressBarModel
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewWidth: CGFloat {
|
||||||
|
graphModel?.diameter ?? CGFloat(64)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var progressLayer = CAShapeLayer()
|
||||||
|
private var tracklayer = CAShapeLayer()
|
||||||
|
private var labelLayer = CATextLayer()
|
||||||
|
|
||||||
|
var progressColor: UIColor = UIColor.red
|
||||||
|
var trackColor: UIColor = UIColor.lightGray
|
||||||
|
|
||||||
|
// A path with which CAShapeLayer will be drawn on the screen
|
||||||
|
private var viewCGPath: CGPath? {
|
||||||
|
|
||||||
|
let width = viewWidth
|
||||||
|
let height = width
|
||||||
|
|
||||||
|
return UIBezierPath(arcCenter: CGPoint(x: width / 2.0, y: height / 2.0),
|
||||||
|
radius: (width - 1.5)/2,
|
||||||
|
startAngle: CGFloat(-0.5 * Double.pi),
|
||||||
|
endAngle: CGFloat(1.5 * Double.pi), clockwise: true).cgPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: setup
|
||||||
|
override open func setupView() {
|
||||||
|
super.setupView()
|
||||||
|
heightConstraint = heightAnchor.constraint(equalToConstant: 0)
|
||||||
|
heightConstraint?.isActive = true
|
||||||
|
widthAnchor.constraint(equalTo: heightAnchor).isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
|
||||||
|
|
||||||
|
super.set(with: model, delegateObject, additionalData)
|
||||||
|
guard let model = model as? CircularProgressBarModel else { return }
|
||||||
|
|
||||||
|
// set background color
|
||||||
|
backgroundColor = model.backgroundColor?.uiColor ?? UIColor.clear
|
||||||
|
|
||||||
|
configureProgressViewToBeCircular()
|
||||||
|
|
||||||
|
// set progress color
|
||||||
|
progressColor = model.color?.uiColor ?? .red
|
||||||
|
progressLayer.strokeColor = progressColor.cgColor
|
||||||
|
|
||||||
|
// set track color
|
||||||
|
trackColor = model.trackColor?.uiColor ?? .lightGray
|
||||||
|
tracklayer.strokeColor = trackColor.cgColor
|
||||||
|
|
||||||
|
// show circular progress view with animation.
|
||||||
|
showProgressWithAnimation(duration: graphModel?.duration ?? 0, value: Float(graphModel?.percent ?? 0) / 100)
|
||||||
|
|
||||||
|
// show progress percentage label.
|
||||||
|
if let drawText = model.drawText, drawText {
|
||||||
|
showProgressPercentage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureProgressViewToBeCircular() {
|
||||||
|
let lineWidth = graphModel?.lineWidth ?? 4.0
|
||||||
|
|
||||||
|
self.drawShape(using: tracklayer, lineWidth: lineWidth)
|
||||||
|
self.drawShape(using: progressLayer, lineWidth: lineWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawShape(using shape: CAShapeLayer, lineWidth: CGFloat) {
|
||||||
|
shape.path = self.viewCGPath
|
||||||
|
shape.fillColor = UIColor.clear.cgColor
|
||||||
|
shape.lineWidth = lineWidth
|
||||||
|
self.layer.addSublayer(shape)
|
||||||
|
}
|
||||||
|
|
||||||
|
// value range is [0,1]
|
||||||
|
private func showProgressWithAnimation(duration: TimeInterval, value: Float) {
|
||||||
|
let animation = CABasicAnimation(keyPath: "strokeEnd")
|
||||||
|
animation.duration = duration
|
||||||
|
|
||||||
|
animation.fromValue = 0 //start animation at point 0
|
||||||
|
animation.toValue = value //end animation at point specified
|
||||||
|
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||||
|
progressLayer.strokeEnd = CGFloat(value)
|
||||||
|
progressLayer.add(animation, forKey: "animateCircle")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showProgressPercentage() {
|
||||||
|
|
||||||
|
let percent = graphModel?.percent ?? 0
|
||||||
|
let percentLen = String(percent).count
|
||||||
|
|
||||||
|
// configure attributed string for progress percentage.
|
||||||
|
let attributedString = NSMutableAttributedString(string: String(percent) + "%")
|
||||||
|
// percent value
|
||||||
|
attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldTitleLarge()], range: NSMakeRange(0, percentLen))
|
||||||
|
// % symbol
|
||||||
|
attributedString.setAttributes([NSAttributedString.Key.font: MFStyler.fontBoldBodyLarge()], range: NSMakeRange(percentLen, 1))
|
||||||
|
|
||||||
|
// show progress percentage in a text layer
|
||||||
|
let width = viewWidth
|
||||||
|
let height = width
|
||||||
|
labelLayer.string = attributedString
|
||||||
|
labelLayer.frame = CGRectMake((width - CGFloat(percentLen * 20))/2, (height - 30)/2, 60, 30)
|
||||||
|
self.layer.addSublayer(labelLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//MARK: MVMCoreUIViewConstrainingProtocol
|
||||||
|
public func needsToBeConstrained() -> Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
119
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift
Normal file
119
MVMCoreUI/Atomic/Atoms/Views/CircularProgressBarModel.swift
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
//
|
||||||
|
// CircularProgressBarModel.swift
|
||||||
|
// MVMCoreUI
|
||||||
|
//
|
||||||
|
// https://oneconfluence.verizon.com/display/MFD/Circular+Progress+Tracker
|
||||||
|
//
|
||||||
|
// Created by Xi Zhang on 7/5/24.
|
||||||
|
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public class CircularProgressBarModel: GraphSizeBase, MoleculeModelProtocol {
|
||||||
|
|
||||||
|
public static var identifier: String = "circularProgress"
|
||||||
|
public var id: String = UUID().uuidString
|
||||||
|
|
||||||
|
public var percent: Int = 0
|
||||||
|
public var diameter: CGFloat? = 64
|
||||||
|
public var lineWidth: CGFloat? = 4
|
||||||
|
public var duration : Double? = 0
|
||||||
|
public var color: Color? = Color(uiColor: UIColor.mfGet(forHex: "#007AB8"))
|
||||||
|
public var trackColor: Color? = Color(uiColor: .mvmCoolGray3)
|
||||||
|
public var drawText: Bool? = true
|
||||||
|
public var backgroundColor: Color? = Color(uiColor: UIColor.clear)
|
||||||
|
|
||||||
|
public override init() {
|
||||||
|
super.init()
|
||||||
|
updateSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case id
|
||||||
|
case moleculeName
|
||||||
|
case percent
|
||||||
|
case size
|
||||||
|
case diameter
|
||||||
|
case lineWidth
|
||||||
|
case duration
|
||||||
|
case color
|
||||||
|
case trackColor
|
||||||
|
case drawText
|
||||||
|
case backgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
required public init(from decoder: Decoder) throws {
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||||
|
|
||||||
|
percent = try typeContainer.decode(Int.self, forKey: .percent)
|
||||||
|
|
||||||
|
if let size = try typeContainer.decodeIfPresent(GraphSize.self, forKey: .size) {
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
updateSize()
|
||||||
|
|
||||||
|
if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) {
|
||||||
|
self.diameter = diameter
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lineWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .lineWidth) {
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
if let duration = try typeContainer.decodeIfPresent(Double.self, forKey: .duration) {
|
||||||
|
self.duration = duration
|
||||||
|
}
|
||||||
|
|
||||||
|
if let drawText = try typeContainer.decodeIfPresent(Bool.self, forKey: .drawText) {
|
||||||
|
self.drawText = drawText
|
||||||
|
}
|
||||||
|
|
||||||
|
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) {
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
if let trackColor = try typeContainer.decodeIfPresent(Color.self, forKey: .trackColor) {
|
||||||
|
self.trackColor = trackColor
|
||||||
|
}
|
||||||
|
|
||||||
|
if let backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) {
|
||||||
|
self.backgroundColor = backgroundColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(to encoder: Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encode(id, forKey: .id)
|
||||||
|
try container.encode(moleculeName, forKey: .moleculeName)
|
||||||
|
try container.encode(percent, forKey: .percent)
|
||||||
|
try container.encodeIfPresent(size, forKey: .size)
|
||||||
|
try container.encodeIfPresent(diameter, forKey: .diameter)
|
||||||
|
try container.encodeIfPresent(lineWidth, forKey: .lineWidth)
|
||||||
|
try container.encodeIfPresent(duration, forKey: .duration)
|
||||||
|
try container.encodeIfPresent(drawText, forKey: .drawText)
|
||||||
|
try container.encodeIfPresent(trackColor, forKey: .trackColor)
|
||||||
|
try container.encodeIfPresent(color, forKey: .color)
|
||||||
|
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func updateSize() {
|
||||||
|
switch size {
|
||||||
|
case .small:
|
||||||
|
diameter = MFSizeObject(standardSize: 64)?.getValueBasedOnApplicationWidth() ?? 64
|
||||||
|
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4
|
||||||
|
break
|
||||||
|
case .medium:
|
||||||
|
diameter = MFSizeObject(standardSize: 84)?.getValueBasedOnApplicationWidth() ?? 84
|
||||||
|
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4
|
||||||
|
break
|
||||||
|
case .large:
|
||||||
|
diameter = MFSizeObject(standardSize: 124)?.getValueBasedOnApplicationWidth() ?? 124
|
||||||
|
lineWidth = MFSizeObject(standardSize: 4)?.getValueBasedOnApplicationWidth() ?? 4
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift
Normal file
29
MVMCoreUI/Atomic/Atoms/Views/GraphSizeProtocol.swift
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// GraphSizeProtocol.swift
|
||||||
|
// MVMCoreUI
|
||||||
|
//
|
||||||
|
// Created by Xi Zhang on 7/15/24.
|
||||||
|
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum GraphSize: String, Codable {
|
||||||
|
case small, medium, large
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol GraphSizeProtocol {
|
||||||
|
var size: GraphSize { get set }
|
||||||
|
func updateSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GraphSizeBase: GraphSizeProtocol {
|
||||||
|
public var size: GraphSize = .small {
|
||||||
|
didSet {
|
||||||
|
updateSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateSize() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,15 +8,11 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public enum GraphSize: String, Codable {
|
|
||||||
case small, medium, large
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum GraphStyle: String, Codable {
|
public enum GraphStyle: String, Codable {
|
||||||
case unlimited, safetyMode
|
case unlimited, safetyMode
|
||||||
}
|
}
|
||||||
|
|
||||||
public class WheelModel: MoleculeModelProtocol {
|
public class WheelModel: GraphSizeBase, MoleculeModelProtocol {
|
||||||
|
|
||||||
public static var identifier: String = "wheel"
|
public static var identifier: String = "wheel"
|
||||||
public var id: String = UUID().uuidString
|
public var id: String = UUID().uuidString
|
||||||
@ -27,11 +23,6 @@ public class WheelModel: MoleculeModelProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var size: GraphSize = .small {
|
|
||||||
didSet {
|
|
||||||
updateSize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public var diameter: CGFloat = 24
|
public var diameter: CGFloat = 24
|
||||||
public var lineWidth: CGFloat = 5
|
public var lineWidth: CGFloat = 5
|
||||||
public var clockwise: Bool = true
|
public var clockwise: Bool = true
|
||||||
@ -39,7 +30,8 @@ public class WheelModel: MoleculeModelProtocol {
|
|||||||
public var colors = [Color]()
|
public var colors = [Color]()
|
||||||
public var backgroundColor: Color?
|
public var backgroundColor: Color?
|
||||||
|
|
||||||
public init() {
|
public override init() {
|
||||||
|
super.init()
|
||||||
updateStyle()
|
updateStyle()
|
||||||
updateSize()
|
updateSize()
|
||||||
}
|
}
|
||||||
@ -58,6 +50,7 @@ public class WheelModel: MoleculeModelProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
required public init(from decoder: Decoder) throws {
|
required public init(from decoder: Decoder) throws {
|
||||||
|
super.init()
|
||||||
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString
|
||||||
|
|
||||||
@ -123,7 +116,7 @@ public class WheelModel: MoleculeModelProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSize() {
|
public override func updateSize() {
|
||||||
switch size {
|
switch size {
|
||||||
case .small:
|
case .small:
|
||||||
diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20
|
diameter = MFSizeObject(standardSize: 20)?.getValueBasedOnApplicationWidth() ?? 20
|
||||||
|
|||||||
@ -108,8 +108,7 @@ extension TabsListItemModel: AddMolecules {
|
|||||||
public func moleculesToAdd() -> AddMolecules.AddParameters? {
|
public func moleculesToAdd() -> AddMolecules.AddParameters? {
|
||||||
guard addedMolecules == nil else { return nil }
|
guard addedMolecules == nil else { return nil }
|
||||||
let index = tabs.selectedIndex
|
let index = tabs.selectedIndex
|
||||||
guard molecules.count >= index else { return nil }
|
guard let addedMolecules = molecules[safe: index] else { return nil }
|
||||||
let addedMolecules = molecules[index]
|
|
||||||
self.addedMolecules = addedMolecules
|
self.addedMolecules = addedMolecules
|
||||||
return (addedMolecules, .below)
|
return (addedMolecules, .below)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,9 @@ open class Carousel: View {
|
|||||||
/// The models for the molecules.
|
/// The models for the molecules.
|
||||||
public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]?
|
public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]?
|
||||||
|
|
||||||
|
/// A list of currently registered cells.
|
||||||
|
public var registeredMoleculeIds: [String]?
|
||||||
|
|
||||||
/// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
|
/// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
|
||||||
public var itemAlignment = UICollectionView.ScrollPosition.left
|
public var itemAlignment = UICollectionView.ScrollPosition.left
|
||||||
|
|
||||||
@ -174,9 +177,7 @@ open class Carousel: View {
|
|||||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)")
|
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] [\(ObjectIdentifier(self).hashValue)]\noriginal model: \(originalModel?.debugDescription ?? "none")\nnew model: \(model)")
|
||||||
|
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
if let originalModel, carouselModel.isDeeplyVisuallyEquivalent(to: originalModel),
|
if hasSameCellRegistration(with: carouselModel, delegateObject: delegateObject) {
|
||||||
originalModel.visibleMolecules.isVisuallyEquivalent(to: molecules ?? []) // Since the carousel model's children are in place replaced and we do not have a deep copy of this model tree, add in this hack to check if the prior captured carousel items match the newly visible ones.
|
|
||||||
{
|
|
||||||
// Prevents a carousel reset while still updating the cell backing data through reconfigureItems.
|
// Prevents a carousel reset while still updating the cell backing data through reconfigureItems.
|
||||||
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is visually equivalent. Skipping rebuild...")
|
MVMCoreLoggingHandler.shared()?.handleDebugMessage("[\(Self.self)] Model is visually equivalent. Skipping rebuild...")
|
||||||
prepareMolecules(with: carouselModel)
|
prepareMolecules(with: carouselModel)
|
||||||
@ -209,6 +210,7 @@ open class Carousel: View {
|
|||||||
|
|
||||||
registerCells(with: carouselModel, delegateObject: delegateObject)
|
registerCells(with: carouselModel, delegateObject: delegateObject)
|
||||||
prepareMolecules(with: carouselModel)
|
prepareMolecules(with: carouselModel)
|
||||||
|
pageIndex = 0
|
||||||
FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate)
|
FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate)
|
||||||
|
|
||||||
setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject)
|
setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject)
|
||||||
@ -249,8 +251,6 @@ open class Carousel: View {
|
|||||||
} else {
|
} else {
|
||||||
loop = false
|
loop = false
|
||||||
}
|
}
|
||||||
|
|
||||||
pageIndex = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open override func reset() {
|
open override func reset() {
|
||||||
@ -284,12 +284,29 @@ open class Carousel: View {
|
|||||||
|
|
||||||
/// Registers the cells with the collection view
|
/// Registers the cells with the collection view
|
||||||
func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) {
|
func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) {
|
||||||
|
var registeredIds = [String]()
|
||||||
for molecule in carouselModel.molecules {
|
for molecule in carouselModel.visibleMolecules {
|
||||||
if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
|
if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
|
||||||
collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier)
|
collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier)
|
||||||
|
registeredIds.append(info.identifier)
|
||||||
|
} else {
|
||||||
|
registeredIds.append(molecule.moleculeName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
registeredMoleculeIds = registeredIds
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasSameCellRegistration(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool {
|
||||||
|
guard let registeredMoleculeIds else { return false }
|
||||||
|
|
||||||
|
let incomingIds = carouselModel.visibleMolecules.map { molecule in
|
||||||
|
if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
|
||||||
|
return info.identifier
|
||||||
|
} else {
|
||||||
|
return molecule.moleculeName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return incomingIds == registeredMoleculeIds
|
||||||
}
|
}
|
||||||
|
|
||||||
//--------------------------------------------------
|
//--------------------------------------------------
|
||||||
@ -361,7 +378,7 @@ open class Carousel: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func trackSwipeActionAnalyticsforIndex(_ index : Int){
|
func trackSwipeActionAnalyticsforIndex(_ index : Int){
|
||||||
guard let itemModel = molecules?[index],
|
guard let itemModel = molecules?[safe:index],
|
||||||
let viewControllerObject = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else { return }
|
let viewControllerObject = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else { return }
|
||||||
MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: viewControllerObject, actionInformation: itemModel.toJSON(), additionalData: nil)
|
MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: viewControllerObject, actionInformation: itemModel.toJSON(), additionalData: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,8 +52,8 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController, Rotor
|
|||||||
bottomView.updateView(width)
|
bottomView.updateView(width)
|
||||||
showFooter(width)
|
showFooter(width)
|
||||||
}
|
}
|
||||||
tableView.visibleCells.forEach { cell in
|
MVMCoreUIUtility.findParentViews(by: (UITableViewCell & MVMCoreViewProtocol).self, views: tableView.subviews).forEach { view in
|
||||||
(cell as? MVMCoreViewProtocol)?.updateView(width)
|
view.updateView(width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import MVMCore
|
import MVMCore
|
||||||
|
import Combine
|
||||||
|
|
||||||
@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol {
|
@objc open class ViewController: UIViewController, MVMCoreViewControllerProtocol, MVMCoreViewManagerViewControllerProtocol, MoleculeDelegateProtocol, FormHolderProtocol, MVMCoreActionDelegateProtocol, ActionDelegateProtocol, MVMCoreLoadDelegateProtocol, UITextFieldDelegate, UITextViewDelegate, ObservingTextFieldDelegate, MVMCoreUIDetailViewProtocol, PageProtocol, PageBehaviorHandlerProtocol {
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ import MVMCore
|
|||||||
public var behaviors: [PageBehaviorProtocol]?
|
public var behaviors: [PageBehaviorProtocol]?
|
||||||
|
|
||||||
public var needsUpdateUI = false
|
public var needsUpdateUI = false
|
||||||
private var observingForResponses: NSObjectProtocol?
|
private var observingForResponses: AnyCancellable?
|
||||||
private var initialLoadFinished = false
|
private var initialLoadFinished = false
|
||||||
public var isFirstRender = true
|
public var isFirstRender = true
|
||||||
public var previousScreenSize = CGSize.zero
|
public var previousScreenSize = CGSize.zero
|
||||||
@ -66,8 +67,27 @@ import MVMCore
|
|||||||
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
|
(pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0)
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
observingForResponses = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil, queue: pageUpdateQueue) { [weak self] notification in
|
observingForResponses = NotificationCenter.default.publisher(for: NSNotification.Name(rawValue: NotificationResponseLoaded))
|
||||||
self?.responseJSONUpdated(notification: notification)
|
.receive(on: self.pageUpdateQueue) // Background serial queue.
|
||||||
|
.compactMap { [weak self] notification in
|
||||||
|
self?.pullUpdates(from: notification) ?? nil
|
||||||
|
}
|
||||||
|
// Merge all page and module updates into one update event.
|
||||||
|
.scan((nil, nil, nil)) { accumulator, next in
|
||||||
|
// Always take the latest page and the latest modules with same key.
|
||||||
|
return (next.0 ?? accumulator.0, next.1 ?? accumulator.1, next.2?.mergingRight(accumulator.2 ?? [:]))
|
||||||
|
}
|
||||||
|
// Delay allowing the previous model update to settle before triggering a re-render.
|
||||||
|
.throttle(for: .seconds(0.25), scheduler: RunLoop.main, latest: true)
|
||||||
|
.sink { [weak self] (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
if let pageUpdates, pageModel != nil {
|
||||||
|
self.loadObject?.pageJSON = pageUpdates
|
||||||
|
}
|
||||||
|
let mergedModuleUpdates = (loadObject?.modulesJSON ?? [:]).mergingLeft(moduleUpdates ?? [:])
|
||||||
|
self.loadObject?.modulesJSON = mergedModuleUpdates
|
||||||
|
self.debugLog("Applying async update page model \(pageModel.debugDescription) and modules \(mergedModuleUpdates.keys) to page.")
|
||||||
|
self.handleNewData(pageModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +97,31 @@ import MVMCore
|
|||||||
self.observingForResponses = nil
|
self.observingForResponses = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pullUpdates(from notification: Notification) -> (pageUpdates: [String : Any]?, pageModel: PageModelProtocol?, moduleUpdates: [String : Any]?)? {
|
||||||
|
// Get the page data.
|
||||||
|
let pageUpdates = extractInterestedPageType(from: notification.userInfo?.optionalDictionaryForKey(KeyPageMap) ?? [:])
|
||||||
|
// Convert the page data into a new model.
|
||||||
|
var pageModel: PageModelProtocol? = nil
|
||||||
|
if let pageUpdates {
|
||||||
|
do {
|
||||||
|
// TODO: Rewiring to parse from plain JSON rather than this protocol indirection.
|
||||||
|
pageModel = try (self as? any TemplateProtocol & PageBehaviorHandlerProtocol & MVMCoreViewControllerProtocol)?.parseTemplate(pageJSON: pageUpdates)
|
||||||
|
} catch {
|
||||||
|
if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: self.pageType))") {
|
||||||
|
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get the module data.
|
||||||
|
let moduleUpdates = extractInterestedModules(from: notification.userInfo?.optionalDictionaryForKey(KeyModuleMap) ?? [:])
|
||||||
|
debugLog("Receiving page \(pageModel?.pageType ?? "none") & \(moduleUpdates?.keys.description ?? "none") modules from \((notification.userInfo?["MVMCoreLoadObject"] as? MVMCoreLoadObject)?.requestParameters?.url?.absoluteString ?? "")")
|
||||||
|
|
||||||
|
guard (pageUpdates != nil && pageModel != nil) || (moduleUpdates != nil && moduleUpdates!.count > 0) else { return nil }
|
||||||
|
|
||||||
|
// Bundle the transformations.
|
||||||
|
return (pageUpdates, pageModel, moduleUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
open func pagesToListenFor() -> [String]? {
|
open func pagesToListenFor() -> [String]? {
|
||||||
guard let pageType = loadObject?.pageType else { return nil }
|
guard let pageType = loadObject?.pageType else { return nil }
|
||||||
return [pageType]
|
return [pageType]
|
||||||
@ -88,52 +133,23 @@ import MVMCore
|
|||||||
return requestModules + behaviorModules
|
return requestModules + behaviorModules
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc open func responseJSONUpdated(notification: Notification) {
|
private func extractInterestedPageType(from pageMap: [String: Any]) -> [String: Any]? {
|
||||||
// Checks for a page we are listening for.
|
guard let pageType = pagesToListenFor()?.first(where: { pageTypeListened -> Bool in
|
||||||
var hasDataUpdate = false
|
guard let page = pageMap.optionalDictionaryForKey(pageTypeListened),
|
||||||
var pageModel: PageModelProtocol? = nil
|
|
||||||
if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap),
|
|
||||||
let loadObject,
|
|
||||||
let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in
|
|
||||||
guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened),
|
|
||||||
let pageType = page.optionalStringForKey(KeyPageType),
|
let pageType = page.optionalStringForKey(KeyPageType),
|
||||||
pageType == pageTypeListened
|
pageType == pageTypeListened
|
||||||
else { return false }
|
else { return false }
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}) {
|
}) else { return nil }
|
||||||
hasDataUpdate = true
|
return pageMap.optionalDictionaryForKey(pageType)
|
||||||
loadObject.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType)
|
|
||||||
|
|
||||||
// Separate page updates from the module updates to avoid unecessary resets to behaviors and full re-renders.
|
|
||||||
do {
|
|
||||||
pageModel = try parsePageJSON(loadObject: loadObject)
|
|
||||||
} catch {
|
|
||||||
if let coreError = MVMCoreErrorObject.createErrorObject(for: error, location: "updateJSON for pageType: \(String(describing: pageType))") {
|
|
||||||
MVMCoreLoggingHandler.shared()?.addError(toLog: coreError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks for modules we are listening for.
|
private func extractInterestedModules(from moduleMap: [String: Any]) -> [String: Any]? {
|
||||||
if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap),
|
guard let modulesListened = modulesToListenFor() else { return nil }
|
||||||
let modulesListened = modulesToListenFor() {
|
return moduleMap.filter { (key: String, value: Any) in
|
||||||
for moduleName in modulesListened {
|
modulesListened.contains { $0 == key }
|
||||||
if let module = modulesLoaded.optionalDictionaryForKey(moduleName) {
|
|
||||||
hasDataUpdate = true
|
|
||||||
var currentModules = loadObject?.modulesJSON ?? [:]
|
|
||||||
currentModules.updateValue(module, forKey: moduleName)
|
|
||||||
loadObject?.modulesJSON = currentModules
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
guard hasDataUpdate else { return }
|
|
||||||
|
|
||||||
MVMCoreDispatchUtility.performBlock(onMainThread: {
|
|
||||||
self.handleNewData(pageModel)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
|
open func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>) -> Bool {
|
||||||
pageType = loadObject.pageType
|
pageType = loadObject.pageType
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
// Copyright © 2020 Verizon Wireless. All rights reserved.
|
||||||
//
|
//
|
||||||
// Form fields are items can be interacted with. They have value, and may need to be validated.
|
// Form fields are items can be interacted with. They have value, and may need to be validated.
|
||||||
|
import VDS
|
||||||
|
|
||||||
public protocol FormFieldProtocol: FormItemProtocol {
|
public protocol FormFieldProtocol: FormItemProtocol {
|
||||||
|
|
||||||
@ -36,6 +36,21 @@ public extension FormFieldProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public protocol FormFieldInternalValidatableProtocol: FormFieldProtocol {
|
||||||
|
associatedtype ValueType = AnyHashable
|
||||||
|
var rules: [AnyRule<ValueType>]? { get set }
|
||||||
|
var internalRules: [RuleAnyModelProtocol]? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FormFieldInternalValidatableProtocol {
|
||||||
|
public var internalRules: [RuleAnyModelProtocol]? {
|
||||||
|
guard let fieldKey else { return nil }
|
||||||
|
return rules?.compactMap{ rule in
|
||||||
|
return RuleVDSModel(field: fieldKey, rule: rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class FormFieldValidity{
|
public class FormFieldValidity{
|
||||||
public var fieldKey: String
|
public var fieldKey: String
|
||||||
public var valid: Bool = true
|
public var valid: Bool = true
|
||||||
|
|||||||
@ -44,6 +44,35 @@ import MVMCore
|
|||||||
if let fieldKey = field.fieldKey {
|
if let fieldKey = field.fieldKey {
|
||||||
fields[fieldKey] = field
|
fields[fieldKey] = field
|
||||||
}
|
}
|
||||||
|
// add internal validators if needed
|
||||||
|
if let field = field as? any FormFieldInternalValidatableProtocol {
|
||||||
|
addInternalRules(field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds additional Rules that are from another source
|
||||||
|
private func addInternalRules(_ field: any FormFieldInternalValidatableProtocol) {
|
||||||
|
if let internalRules = field.internalRules, !internalRules.isEmpty {
|
||||||
|
|
||||||
|
//find the group
|
||||||
|
if let formGroup = formRules?.first(where: {$0.groupName == field.groupName}) {
|
||||||
|
var appendingRules = [RulesProtocol]()
|
||||||
|
internalRules.forEach { internalRule in
|
||||||
|
if !formGroup.rules.contains(where: { internalRule.type == $0.type && internalRule.fields == $0.fields } ) {
|
||||||
|
appendingRules.append(internalRule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
formGroup.rules.append(contentsOf: internalRules)
|
||||||
|
} else {
|
||||||
|
//create the new group
|
||||||
|
let formGroup = FormGroupRule(field.groupName, internalRules, [])
|
||||||
|
if var formRules {
|
||||||
|
formRules.append(formGroup)
|
||||||
|
} else {
|
||||||
|
formRules = [formGroup]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds the form action to the validator.
|
/// Adds the form action to the validator.
|
||||||
@ -72,7 +101,6 @@ import MVMCore
|
|||||||
if let validator = delegate?.formValidator {
|
if let validator = delegate?.formValidator {
|
||||||
validator.delegate = delegate
|
validator.delegate = delegate
|
||||||
validator.insert(item)
|
validator.insert(item)
|
||||||
|
|
||||||
// TODO: Temporary hacks, rewrite architecture to support this.
|
// TODO: Temporary hacks, rewrite architecture to support this.
|
||||||
_ = validator.validate()
|
_ = validator.validate()
|
||||||
}
|
}
|
||||||
|
|||||||
63
MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift
Normal file
63
MVMCoreUI/FormUIHelpers/Rules/Rules/RuleVDSModel.swift
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
//
|
||||||
|
// RuleVDSModel.swift
|
||||||
|
// MVMCoreUI
|
||||||
|
//
|
||||||
|
// Created by Matt Bruce on 7/12/24.
|
||||||
|
// Copyright © 2024 Verizon Wireless. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import VDS
|
||||||
|
|
||||||
|
open class VDSRuleBase: RuleAnyModelProtocol {
|
||||||
|
open var ruleId: String?
|
||||||
|
open var ruleType: String
|
||||||
|
open var errorMessage: [String : String]?
|
||||||
|
open var fields = [String]()
|
||||||
|
public init(){
|
||||||
|
ruleType = Self.identifier
|
||||||
|
}
|
||||||
|
public var type: String { ruleType }
|
||||||
|
open func isValid(_ formField: any FormFieldProtocol) -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
public static var identifier: String = "AnyVDSRule"
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RuleVDSModel<ValueType>: VDSRuleBase {
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Properties
|
||||||
|
//--------------------------------------------------
|
||||||
|
public var rule: AnyRule<ValueType>
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Initializer
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
public init(field: String, rule: AnyRule<ValueType>) {
|
||||||
|
self.rule = rule
|
||||||
|
super.init()
|
||||||
|
self.ruleType = rule.ruleType
|
||||||
|
self.fields = [field]
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: any Decoder) throws {
|
||||||
|
fatalError("init(from:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------
|
||||||
|
// MARK: - Validation
|
||||||
|
//--------------------------------------------------
|
||||||
|
|
||||||
|
public override func isValid(_ formField: FormFieldProtocol) -> Bool {
|
||||||
|
let value = formField.formFieldValue() as? ValueType
|
||||||
|
let valid = rule.isValid(value: value)
|
||||||
|
if let field = fields.first, !valid {
|
||||||
|
errorMessage = [field: rule.errorMessage]
|
||||||
|
} else {
|
||||||
|
errorMessage = nil
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ open class CoreUIModelMapping: ModelMapping {
|
|||||||
ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self)
|
ModelRegistry.register(handler: ButtonGroup.self, for: ButtonGroupModel.self)
|
||||||
|
|
||||||
// MARK:- Entry Field
|
// MARK:- Entry Field
|
||||||
ModelRegistry.register(handler: TextEntryField.self, for: TextEntryFieldModel.self)
|
ModelRegistry.register(handler: InputEntryField.self, for: TextEntryFieldModel.self)
|
||||||
ModelRegistry.register(handler: MdnEntryField.self, for: MdnEntryFieldModel.self)
|
ModelRegistry.register(handler: MdnEntryField.self, for: MdnEntryFieldModel.self)
|
||||||
ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self)
|
ModelRegistry.register(handler: DigitEntryField.self, for: DigitEntryFieldModel.self)
|
||||||
ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self)
|
ModelRegistry.register(handler: ItemDropdownEntryField.self, for: ItemDropdownEntryFieldModel.self)
|
||||||
@ -67,6 +67,7 @@ open class CoreUIModelMapping: ModelMapping {
|
|||||||
ModelRegistry.register(handler: LoadImageView.self, for: ImageViewModel.self)
|
ModelRegistry.register(handler: LoadImageView.self, for: ImageViewModel.self)
|
||||||
ModelRegistry.register(handler: Line.self, for: LineModel.self)
|
ModelRegistry.register(handler: Line.self, for: LineModel.self)
|
||||||
ModelRegistry.register(handler: Wheel.self, for: WheelModel.self)
|
ModelRegistry.register(handler: Wheel.self, for: WheelModel.self)
|
||||||
|
ModelRegistry.register(handler: CircularProgressBar.self, for: CircularProgressBarModel.self)
|
||||||
ModelRegistry.register(handler: Toggle.self, for: ToggleModel.self)
|
ModelRegistry.register(handler: Toggle.self, for: ToggleModel.self)
|
||||||
ModelRegistry.register(handler: CheckboxLabel.self, for: CheckboxLabelModel.self)
|
ModelRegistry.register(handler: CheckboxLabel.self, for: CheckboxLabelModel.self)
|
||||||
ModelRegistry.register(handler: Arrow.self, for: ArrowModel.self)
|
ModelRegistry.register(handler: Arrow.self, for: ArrowModel.self)
|
||||||
|
|||||||
@ -60,6 +60,16 @@ public extension MVMCoreUIUtility {
|
|||||||
return findViews(by: type, views: queue, excludedViews: excludedViews) + matching
|
return findViews(by: type, views: queue, excludedViews: excludedViews) + matching
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func findParentViews<T>(by type: T.Type, views: [UIView]) -> [T] {
|
||||||
|
return views.reduce(into: [T]()) { matchingViews, view in
|
||||||
|
if let view = view as? T {
|
||||||
|
return matchingViews.append(view) // If this view is the type stop here and return, ignoring its children.
|
||||||
|
}
|
||||||
|
// Otherwise check downstream.
|
||||||
|
matchingViews += findParentViews(by: type, views: view.subviews)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
static func visibleNavigationBarStlye() -> NavigationItemStyle? {
|
static func visibleNavigationBarStlye() -> NavigationItemStyle? {
|
||||||
if let navController = NavigationController.navigationController(),
|
if let navController = NavigationController.navigationController(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user