Merge branch 'develop' into feature/toolbar_entry_field

This commit is contained in:
Kevin G Christiano 2020-04-13 08:18:36 -04:00
commit bcb296fa5d
41 changed files with 1730 additions and 241 deletions

View File

@ -203,6 +203,9 @@
BB6C6AC1242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6C6ABF242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift */; };
BB6C6AC824225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6C6AC62422528F005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift */; };
BB6C6AC924225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShortModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB6C6AC72422528F005F7224 /* ListOneColumnTextWithWhitespaceDividerShortModel.swift */; };
BBAA4F03243D8E3B005AAD5F /* RadioBoxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAA4EFF243D8E3B005AAD5F /* RadioBoxes.swift */; };
BBAA4F04243D8E3B005AAD5F /* RadioBoxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAA4F00243D8E3B005AAD5F /* RadioBoxModel.swift */; };
BBAA4F05243D8E3B005AAD5F /* RadioBoxesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAA4F01243D8E3B005AAD5F /* RadioBoxesModel.swift */; };
BBBBC87C24374A4900B0F079 /* ListThreeColumnBillChangesDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBBC87A24374A4900B0F079 /* ListThreeColumnBillChangesDivider.swift */; };
BBBBC87D24374A4900B0F079 /* ListThreeColumnBillChangesDividerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBBC87B24374A4900B0F079 /* ListThreeColumnBillChangesDividerModel.swift */; };
C003506123AA94CD00B6AC29 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003506023AA94CD00B6AC29 /* Button.swift */; };
@ -223,6 +226,10 @@
D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */; };
D20FB165241A5D75004AFC3A /* NavigationItemModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FB164241A5D75004AFC3A /* NavigationItemModelProtocol.swift */; };
D213347723843825008E41B3 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213347623843825008E41B3 /* Line.swift */; };
D21B7F71243BAC1600051ABF /* CollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21B7F70243BAC1600051ABF /* CollectionViewCell.swift */; };
D21B7F73243BAC6800051ABF /* CollectionItemModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21B7F72243BAC6800051ABF /* CollectionItemModelProtocol.swift */; };
D21B7F75243BAC8900051ABF /* CarouselItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21B7F74243BAC8900051ABF /* CarouselItem.swift */; };
D21B7F77243BB70700051ABF /* MoleculeCollectionItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21B7F76243BB70700051ABF /* MoleculeCollectionItemModel.swift */; };
D21EE53C23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D21EE53B23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift */; };
D224798A2314445E003FCCF9 /* LabelToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22479892314445E003FCCF9 /* LabelToggle.swift */; };
D224798C231450C8003FCCF9 /* HeadlineBodyToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224798B231450C8003FCCF9 /* HeadlineBodyToggle.swift */; };
@ -255,6 +262,15 @@
D260D7B122D65BDD007E7233 /* MVMCoreUIPageControl.h in Headers */ = {isa = PBXBuildFile; fileRef = D260D7AF22D65BDD007E7233 /* MVMCoreUIPageControl.h */; settings = {ATTRIBUTES = (Public, ); }; };
D260D7B222D65BDD007E7233 /* MVMCoreUIPageControl.m in Sources */ = {isa = PBXBuildFile; fileRef = D260D7B022D65BDD007E7233 /* MVMCoreUIPageControl.m */; };
D260D7B622D68514007E7233 /* MVMCoreUIPagingProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
D264FA8C243BCD8E00D98315 /* CollectionTemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FA8B243BCD8E00D98315 /* CollectionTemplateModel.swift */; };
D264FA8E243BCD9A00D98315 /* CollectionTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FA8D243BCD9A00D98315 /* CollectionTemplate.swift */; };
D264FA90243BCE6800D98315 /* ThreeLayerCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FA8F243BCE6800D98315 /* ThreeLayerCollectionViewController.swift */; };
D264FAA1243CF66B00D98315 /* ContainerCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FAA0243CF66B00D98315 /* ContainerCollectionReusableView.swift */; };
D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FAA2243E632F00D98315 /* ProgrammaticCollectionViewController.swift */; };
D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FAA4243F66A500D98315 /* CollectionTemplateItemProtocol.swift */; };
D264FAA7243FE13B00D98315 /* RadioBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FAA6243FE13B00D98315 /* RadioBox.swift */; };
D264FAAA2440F97600D98315 /* CollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FAA92440F97600D98315 /* CollectionView.swift */; };
D264FAAC2441009400D98315 /* RadioBoxCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D264FAAB2441009400D98315 /* RadioBoxCollectionViewCell.swift */; };
D268C70C2386DFFD007F2C1C /* MoleculeStackItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EB368A23609801006832FA /* MoleculeStackItemModel.swift */; };
D268C70E238C22D7007F2C1C /* DropDownFilterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D268C70D238C22D7007F2C1C /* DropDownFilterTableViewCell.swift */; };
D26C5A6B23F4A40D007AEECE /* ListItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D26C5A6A23F4A40D007AEECE /* ListItemModel.swift */; };
@ -614,6 +630,9 @@
BB6C6ABF242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListOneColumnTextWithWhitespaceDividerTall.swift; sourceTree = "<group>"; };
BB6C6AC62422528F005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListOneColumnTextWithWhitespaceDividerShort.swift; sourceTree = "<group>"; };
BB6C6AC72422528F005F7224 /* ListOneColumnTextWithWhitespaceDividerShortModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListOneColumnTextWithWhitespaceDividerShortModel.swift; sourceTree = "<group>"; };
BBAA4EFF243D8E3B005AAD5F /* RadioBoxes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioBoxes.swift; sourceTree = "<group>"; };
BBAA4F00243D8E3B005AAD5F /* RadioBoxModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioBoxModel.swift; sourceTree = "<group>"; };
BBAA4F01243D8E3B005AAD5F /* RadioBoxesModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioBoxesModel.swift; sourceTree = "<group>"; };
BBBBC87A24374A4900B0F079 /* ListThreeColumnBillChangesDivider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListThreeColumnBillChangesDivider.swift; sourceTree = "<group>"; };
BBBBC87B24374A4900B0F079 /* ListThreeColumnBillChangesDividerModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListThreeColumnBillChangesDividerModel.swift; sourceTree = "<group>"; };
C003506023AA94CD00B6AC29 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = "<group>"; };
@ -634,6 +653,10 @@
D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoButtonView.swift; sourceTree = "<group>"; };
D20FB164241A5D75004AFC3A /* NavigationItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModelProtocol.swift; sourceTree = "<group>"; };
D213347623843825008E41B3 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
D21B7F70243BAC1600051ABF /* CollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewCell.swift; sourceTree = "<group>"; };
D21B7F72243BAC6800051ABF /* CollectionItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionItemModelProtocol.swift; sourceTree = "<group>"; };
D21B7F74243BAC8900051ABF /* CarouselItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselItem.swift; sourceTree = "<group>"; };
D21B7F76243BB70700051ABF /* MoleculeCollectionItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeCollectionItemModel.swift; sourceTree = "<group>"; };
D21EE53B23AD3AD4003D1A30 /* NSLayoutConstraintAxis+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSLayoutConstraintAxis+Extension.swift"; sourceTree = "<group>"; };
D22479892314445E003FCCF9 /* LabelToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelToggle.swift; sourceTree = "<group>"; };
D224798B231450C8003FCCF9 /* HeadlineBodyToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlineBodyToggle.swift; sourceTree = "<group>"; };
@ -666,6 +689,15 @@
D260D7AF22D65BDD007E7233 /* MVMCoreUIPageControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIPageControl.h; sourceTree = "<group>"; };
D260D7B022D65BDD007E7233 /* MVMCoreUIPageControl.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUIPageControl.m; sourceTree = "<group>"; };
D260D7B522D68509007E7233 /* MVMCoreUIPagingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIPagingProtocol.h; sourceTree = "<group>"; };
D264FA8B243BCD8E00D98315 /* CollectionTemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionTemplateModel.swift; sourceTree = "<group>"; };
D264FA8D243BCD9A00D98315 /* CollectionTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionTemplate.swift; sourceTree = "<group>"; };
D264FA8F243BCE6800D98315 /* ThreeLayerCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeLayerCollectionViewController.swift; sourceTree = "<group>"; };
D264FAA0243CF66B00D98315 /* ContainerCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerCollectionReusableView.swift; sourceTree = "<group>"; };
D264FAA2243E632F00D98315 /* ProgrammaticCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgrammaticCollectionViewController.swift; sourceTree = "<group>"; };
D264FAA4243F66A500D98315 /* CollectionTemplateItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionTemplateItemProtocol.swift; sourceTree = "<group>"; };
D264FAA6243FE13B00D98315 /* RadioBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioBox.swift; sourceTree = "<group>"; };
D264FAA92440F97600D98315 /* CollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionView.swift; sourceTree = "<group>"; };
D264FAAB2441009400D98315 /* RadioBoxCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RadioBoxCollectionViewCell.swift; sourceTree = "<group>"; };
D268C70D238C22D7007F2C1C /* DropDownFilterTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropDownFilterTableViewCell.swift; sourceTree = "<group>"; };
D26C5A6A23F4A40D007AEECE /* ListItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListItemModel.swift; sourceTree = "<group>"; };
D274CA322236A78900B01B62 /* FooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FooterView.swift; sourceTree = "<group>"; };
@ -922,6 +954,7 @@
0A5D59C323AD488600EFD9E9 /* Protocols */ = {
isa = PBXGroup;
children = (
D21B7F72243BAC6800051ABF /* CollectionItemModelProtocol.swift */,
0A5D59C123AD2F5700EFD9E9 /* AppleGuidelinesProtocol.swift */,
);
path = Protocols;
@ -1265,8 +1298,6 @@
D26C5A6A23F4A40D007AEECE /* ListItemModel.swift */,
01EB368923609801006832FA /* MoleculeListItemModel.swift */,
01509D8E2327EC6F00EF99AA /* MoleculeTableViewCell.swift */,
012A88C1238D7BCA00FE3DA1 /* CarouselItemModel.swift */,
D2A6390422CBCE160052ED1F /* MoleculeCollectionViewCell.swift */,
D28A838023CCB0D800DFE4FC /* AccordionListItemModel.swift */,
D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */,
D28A837C23CCA86A00DFE4FC /* TabsListItemModel.swift */,
@ -1278,6 +1309,10 @@
D260105E23D0BFFC00764D80 /* StackItem.swift */,
01EB368A23609801006832FA /* MoleculeStackItemModel.swift */,
D2FB151C23A40F1500C20E10 /* MoleculeStackItem.swift */,
D21B7F76243BB70700051ABF /* MoleculeCollectionItemModel.swift */,
D2A6390422CBCE160052ED1F /* MoleculeCollectionViewCell.swift */,
012A88C1238D7BCA00FE3DA1 /* CarouselItemModel.swift */,
D21B7F74243BAC8900051ABF /* CarouselItem.swift */,
);
path = Items;
sourceTree = "<group>";
@ -1384,6 +1419,23 @@
path = Doughnut;
sourceTree = "<group>";
};
D264FAA8243FE17A00D98315 /* Selectors */ = {
isa = PBXGroup;
children = (
D264FAAB2441009400D98315 /* RadioBoxCollectionViewCell.swift */,
BBAA4F01243D8E3B005AAD5F /* RadioBoxesModel.swift */,
BBAA4EFF243D8E3B005AAD5F /* RadioBoxes.swift */,
BBAA4F00243D8E3B005AAD5F /* RadioBoxModel.swift */,
D264FAA6243FE13B00D98315 /* RadioBox.swift */,
0116A4E4228B19640094F3ED /* RadioButtonSelectionHelper.swift */,
011D95AE2407266E000E3791 /* RadioButtonModel.swift */,
01004F2F22721C3800991ECC /* RadioButton.swift */,
31BE15CA23D8924C00452370 /* CheckboxModel.swift */,
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */,
);
path = Selectors;
sourceTree = "<group>";
};
D29DF0C221E404D4003B2FB9 = {
isa = PBXGroup;
children = (
@ -1440,6 +1492,9 @@
942C378B2412F4FA0066E45E /* ModalMoleculeListTemplate.swift */,
014AA72A23C5059B006F3E93 /* ThreeLayerPageTemplateModel.swift */,
D2D6CD4122E78FAB00D701B8 /* ThreeLayerTemplate.swift */,
D264FAA4243F66A500D98315 /* CollectionTemplateItemProtocol.swift */,
D264FA8B243BCD8E00D98315 /* CollectionTemplateModel.swift */,
D264FA8D243BCD9A00D98315 /* CollectionTemplate.swift */,
);
path = Templates;
sourceTree = "<group>";
@ -1456,6 +1511,7 @@
D29DF10D21E67A70003B2FB9 /* Atoms */ = {
isa = PBXGroup;
children = (
D264FAA8243FE17A00D98315 /* Selectors */,
D29DF22B21E6A0FA003B2FB9 /* TextFields */,
D29DF17D21E69E26003B2FB9 /* Views */,
D29DF16821E69E1F003B2FB9 /* Buttons */,
@ -1495,10 +1551,13 @@
D29DF2CD21E7C104003B2FB9 /* MFLoadingViewController.m */,
D2A5146A2214905000345BFB /* ThreeLayerViewController.swift */,
D2E1FADE2268B8E700AEFD8C /* ThreeLayerTableViewController.swift */,
D264FAA0243CF66B00D98315 /* ContainerCollectionReusableView.swift */,
D264FA8F243BCE6800D98315 /* ThreeLayerCollectionViewController.swift */,
D2C521A823EDE79E00CA2634 /* ViewController.swift */,
D2A92881241AAB67004E01C6 /* ScrollingViewController.swift */,
D2A92883241ACB25004E01C6 /* ProgrammaticScrollViewController.swift */,
D2A92885241ACD99004E01C6 /* ProgrammaticTableViewController.swift */,
D264FAA2243E632F00D98315 /* ProgrammaticCollectionViewController.swift */,
);
path = BaseControllers;
sourceTree = "<group>";
@ -1595,9 +1654,6 @@
DBC4391A224421A0001AB423 /* CaretLink.swift */,
D28A838A23CCDA6B00DFE4FC /* ButtonModel.swift */,
D2E2A99E23E07F8A000B42E6 /* PillButton.swift */,
0116A4E4228B19640094F3ED /* RadioButtonSelectionHelper.swift */,
011D95AE2407266E000E3791 /* RadioButtonModel.swift */,
01004F2F22721C3800991ECC /* RadioButton.swift */,
);
path = Buttons;
sourceTree = "<group>";
@ -1621,8 +1677,6 @@
017BEB7A236763000024EF95 /* LineModel.swift */,
D213347623843825008E41B3 /* Line.swift */,
94C2D9822386F3E30006CF46 /* Label */,
31BE15CA23D8924C00452370 /* CheckboxModel.swift */,
0A7BAFA0232BE61800FB8E22 /* Checkbox.swift */,
31BE15C923D8924C00452370 /* CheckboxLabelModel.swift */,
0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */,
D28A838223CCBD3F00DFE4FC /* CircleProgressModel.swift */,
@ -1796,6 +1850,8 @@
D2B18B802360945C00A9AEDC /* View.swift */,
0AE14F63238315D2005417F8 /* TextField.swift */,
D2755D7A23689C7500485468 /* TableViewCell.swift */,
D21B7F70243BAC1600051ABF /* CollectionViewCell.swift */,
D264FAA92440F97600D98315 /* CollectionView.swift */,
0A5D59C323AD488600EFD9E9 /* Protocols */,
);
path = BaseClasses;
@ -2005,6 +2061,7 @@
AA56A20F243C5EE900303286 /* ListTwoColumnSubsectionDividerModel.swift in Sources */,
94C2D9A923872E5E0006CF46 /* LabelAttributeImageModel.swift in Sources */,
DBC4391922442197001AB423 /* DashLine.swift in Sources */,
D264FAAA2440F97600D98315 /* CollectionView.swift in Sources */,
0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */,
D2FB151D23A40F1500C20E10 /* MoleculeStackItem.swift in Sources */,
AA11A41F23F15D3100D7962F /* ListRightVariablePayments.swift in Sources */,
@ -2045,8 +2102,10 @@
D2E2A98323D8B32D000B42E6 /* EyebrowHeadlineBodyLinkModel.swift in Sources */,
012A88AD238C418100FE3DA1 /* TemplateProtocol.swift in Sources */,
BB6C6AC1242232DF005F7224 /* ListOneColumnTextWithWhitespaceDividerTall.swift in Sources */,
D21B7F77243BB70700051ABF /* MoleculeCollectionItemModel.swift in Sources */,
D29DF2B421E7B76D003B2FB9 /* MFLoadingSpinner.m in Sources */,
011D9602240DA20A000E3791 /* FormRuleWatcherFieldProtocol.swift in Sources */,
D264FAA1243CF66B00D98315 /* ContainerCollectionReusableView.swift in Sources */,
D260106323D0C05000764D80 /* StackItemModel.swift in Sources */,
D2E2A99823D8D63C000B42E6 /* ActionDetailWithImageModel.swift in Sources */,
BBBBC87D24374A4900B0F079 /* ListThreeColumnBillChangesDividerModel.swift in Sources */,
@ -2062,8 +2121,10 @@
D28A837B23C928DA00DFE4FC /* MoleculeListCellProtocol.swift in Sources */,
014AA72F23C5059B006F3E93 /* ThreeLayerPageTemplateModel.swift in Sources */,
0A21DB91235E0EDB00C160A2 /* DigitBox.swift in Sources */,
BBAA4F04243D8E3B005AAD5F /* RadioBoxModel.swift in Sources */,
D22D1F572204CE5D0077CEC0 /* MVMCoreUIStackableViewController.m in Sources */,
D28A838B23CCDA6B00DFE4FC /* ButtonModel.swift in Sources */,
D21B7F71243BAC1600051ABF /* CollectionViewCell.swift in Sources */,
D2A5145F2211DDC100345BFB /* MoleculeStackView.swift in Sources */,
C7F8012323E846C300396FBD /* ListRVWheelModel.swift in Sources */,
D29DF27621E79E81003B2FB9 /* MVMCoreUILoggingHandler.m in Sources */,
@ -2114,11 +2175,13 @@
011D959B240451E3000E3791 /* RuleRequiredModel.swift in Sources */,
526A265C240D1FF700B0D828 /* ListTwoColumnCompareChangesModel.swift in Sources */,
D2A92886241ACD99004E01C6 /* ProgrammaticTableViewController.swift in Sources */,
BBAA4F05243D8E3B005AAD5F /* RadioBoxesModel.swift in Sources */,
01509D952327ED1900EF99AA /* HeadlineBodyLinkToggle.swift in Sources */,
31BE15CB23D8924D00452370 /* CheckboxLabelModel.swift in Sources */,
D29DF13021E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m in Sources */,
5248BFEC23F12E350059236A /* ListThreeColumnPlanDataDivider.swift in Sources */,
0ABD136D237CAD1E0081388D /* DateDropdownEntryField.swift in Sources */,
D264FA8E243BCD9A00D98315 /* CollectionTemplate.swift in Sources */,
0A7EF85B23D8A52800B2AAD1 /* EntryFieldModel.swift in Sources */,
8DEFA95C243DAC20000D27E5 /* ListThreeColumnDataUsageDividerModel.swift in Sources */,
94F217B723E0BF6100A47C06 /* PrimaryButtonView.m in Sources */,
@ -2181,11 +2244,13 @@
C6FA7D5323C77A4A00A3614A /* StringAndMoleculeStack.swift in Sources */,
011D958524042432000E3791 /* RulesProtocol.swift in Sources */,
94AF4A3F23E9D13900676048 /* MFCaretButton.m in Sources */,
D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */,
D29DF27A21E7A533003B2FB9 /* MVMCoreUISession.m in Sources */,
D2A5146B2214905000345BFB /* ThreeLayerViewController.swift in Sources */,
526A265E240D200500B0D828 /* ListTwoColumnCompareChanges.swift in Sources */,
8D24041523E7FC0B009E23BE /* ListLeftVariableIconWithRightCaretModel.swift in Sources */,
D28A838F23CCDEDE00DFE4FC /* TwoButtonViewModel.swift in Sources */,
D264FAAC2441009400D98315 /* RadioBoxCollectionViewCell.swift in Sources */,
BB2C969224330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift in Sources */,
D2D90B42240463E100DD6EC9 /* MoleculeHeaderModel.swift in Sources */,
012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */,
@ -2209,6 +2274,7 @@
8D084AD02410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift in Sources */,
0ABD1371237DB0450081388D /* ItemDropdownEntryField.swift in Sources */,
8D24041123E7FB9E009E23BE /* ListLeftVariableIconWithRightCaret.swift in Sources */,
BBAA4F03243D8E3B005AAD5F /* RadioBoxes.swift in Sources */,
D2E1FAE12268E81D00AEFD8C /* MoleculeListTemplate.swift in Sources */,
525019E72406853600EED91C /* ListFourColumnDataUsageDivider.swift in Sources */,
0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */,
@ -2232,6 +2298,7 @@
01EB3684236097C0006832FA /* MoleculeModelProtocol.swift in Sources */,
D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */,
8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */,
D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */,
D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */,
8DD1E36E243B3CFB00D8F2DF /* ListThreeColumnInternationalDataModel.swift in Sources */,
D243859923A16B1800332775 /* Container.swift in Sources */,
@ -2254,6 +2321,7 @@
D2B1E3E522F37D6A0065F95C /* ImageHeadlineBody.swift in Sources */,
0A21DB94235E24ED00C160A2 /* DigitEntryField.swift in Sources */,
AA56A211243C5EFC00303286 /* ListTwoColumnSubsectionDivider.swift in Sources */,
D264FA8C243BCD8E00D98315 /* CollectionTemplateModel.swift in Sources */,
52B201D224081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethod.swift in Sources */,
D26C5A6B23F4A40D007AEECE /* ListItemModel.swift in Sources */,
0A21DB8D235E06EF00C160A2 /* MFDigitTextField.m in Sources */,
@ -2280,10 +2348,12 @@
52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */,
C003506123AA94CD00B6AC29 /* Button.swift in Sources */,
DBC4391B224421A0001AB423 /* CaretLink.swift in Sources */,
D264FA90243BCE6800D98315 /* ThreeLayerCollectionViewController.swift in Sources */,
0198F7A82256A80B0066C936 /* MFRadioButton.m in Sources */,
0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */,
BB6C6AC824225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShort.swift in Sources */,
0A41BA6E2344FCD400D4C0BC /* CATransaction+Extension.swift in Sources */,
D21B7F73243BAC6800051ABF /* CollectionItemModelProtocol.swift in Sources */,
C7192E7D23C301750050C2A0 /* HeadLineBodyCaretLinkImage.swift in Sources */,
D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */,
D29DF29C21E7ADB9003B2FB9 /* MFProgrammaticTableViewController.m in Sources */,
@ -2313,6 +2383,7 @@
EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */,
0105618D224BBE7700E1557D /* FormValidator.swift in Sources */,
01509D912327ECE600EF99AA /* CornerLabels.swift in Sources */,
D21B7F75243BAC8900051ABF /* CarouselItem.swift in Sources */,
D22D1F1B220341F60077CEC0 /* MVMCoreUICheckBox.m in Sources */,
C695A69823C990C200BFB94E /* DoughnutChartView.swift in Sources */,
8D3BA9BD2433787000D341BA /* ListThreeColumnInternationalDataDividerModel.swift in Sources */,
@ -2323,6 +2394,7 @@
D260106523D0CEA700764D80 /* StackModel.swift in Sources */,
0A6682AA2435125F00AD3CA1 /* Styler.swift in Sources */,
D29770F521F7C6D600B2F0D0 /* TopLabelsAndBottomButtonsViewController.m in Sources */,
D264FAA7243FE13B00D98315 /* RadioBox.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -0,0 +1,183 @@
//
// RadioBox.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/9/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class RadioBox: Control {
public let label = Label.createLabelRegularBodySmall(true)
public let subTextLabel = Label.createLabelRegularMicro(true)
public var isOutOfStock = false
public var accentColor = UIColor.mvmRed
public let innerPadding: CGFloat = 12.0
private var borderLayer: CALayer?
private var strikeLayer: CALayer?
private var maskLayer: CALayer?
public var subTextLabelHeightConstraint: NSLayoutConstraint?
public var radioBoxModel: RadioBoxModel? {
return model as? RadioBoxModel
}
// MARK: - MVMCoreViewProtocol
open override func updateView(_ size: CGFloat) {
super.updateView(size)
label.updateView(size)
subTextLabel.updateView(size)
layer.setNeedsDisplay()
}
open override func setupView() {
super.setupView()
layer.delegate = self
layer.borderColor = UIColor.black.cgColor
layer.borderWidth = 1
label.numberOfLines = 1
addSubview(label)
NSLayoutConstraint.constraintPinSubview(label, pinTop: true, topConstant: innerPadding, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding)
subTextLabel.textColor = .mvmCoolGray6
subTextLabel.numberOfLines = 1
addSubview(subTextLabel)
NSLayoutConstraint.constraintPinSubview(subTextLabel, pinTop: false, topConstant:0, pinBottom: false, bottomConstant: 0, pinLeft: true, leftConstant: innerPadding, pinRight: true, rightConstant: innerPadding)
bottomAnchor.constraint(greaterThanOrEqualTo: subTextLabel.bottomAnchor, constant: innerPadding).isActive = true
subTextLabel.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 2).isActive = true
subTextLabelHeightConstraint = subTextLabel.heightAnchor.constraint(equalToConstant: 0)
subTextLabelHeightConstraint?.isActive = true
addTarget(self, action: #selector(selectBox), for: .touchUpInside)
}
// MARK: - MoleculeViewProtocol
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let model = model as? RadioBoxModel else { return }
isSelected = model.selected
isEnabled = model.enabled
label.text = model.text
subTextLabel.text = model.subText
isOutOfStock = model.strikethrough
subTextLabelHeightConstraint?.isActive = (subTextLabel.text?.count ?? 0) == 0
}
// MARK: - State Handling
open override func draw(_ layer: CALayer, in ctx: CGContext) {
// Draw the strikethrough
strikeLayer?.removeFromSuperlayer()
if isOutOfStock {
let line = getStrikeThrough(color: .black, thickness: 1)
layer.addSublayer(line)
strikeLayer = line
}
// Draw the border
borderLayer?.removeFromSuperlayer()
if isSelected {
layer.borderWidth = 0
let border = getSelectedBorder()
layer.addSublayer(border)
borderLayer = border
} else {
layer.borderWidth = 1
}
// Handle Mask
maskLayer?.removeFromSuperlayer()
if !isEnabled {
let mask = getMaskLayer()
layer.mask = mask
}
}
open override func layoutSubviews() {
super.layoutSubviews()
// Accounts for any size changes
layer.setNeedsDisplay()
}
@objc open func selectBox() {
isSelected = true
radioBoxModel?.selected = isSelected
layer.setNeedsDisplay()
}
@objc open func deselectBox() {
isSelected = false
radioBoxModel?.selected = isSelected
layer.setNeedsDisplay()
}
/// Gets the selected state border
func getSelectedBorder() -> CAShapeLayer {
let layer = CAShapeLayer()
let topLineWidth: CGFloat = 4
let topLinePath = UIBezierPath()
topLinePath.lineWidth = topLineWidth
topLinePath.move(to: CGPoint(x: 0, y: topLineWidth / 2.0))
topLinePath.addLine(to: CGPoint(x: bounds.width, y: topLineWidth / 2.0))
let topLineLayer = CAShapeLayer()
topLineLayer.fillColor = nil
topLineLayer.strokeColor = UIColor.mvmRed.cgColor
topLineLayer.lineWidth = 4
topLineLayer.path = topLinePath.cgPath
layer.addSublayer(topLineLayer)
let lineWidth: CGFloat = 1
let halfLineWidth: CGFloat = 0.5
let linePath = UIBezierPath()
linePath.move(to: CGPoint(x: halfLineWidth, y: topLineWidth))
linePath.addLine(to: CGPoint(x: halfLineWidth, y: bounds.height))
linePath.move(to: CGPoint(x: 0, y: bounds.height - halfLineWidth))
linePath.addLine(to: CGPoint(x: bounds.width, y: bounds.height - halfLineWidth))
linePath.move(to: CGPoint(x: bounds.width - halfLineWidth, y: bounds.height))
linePath.addLine(to: CGPoint(x: bounds.width - halfLineWidth, y: topLineWidth))
let borderLayer = CAShapeLayer()
borderLayer.fillColor = nil
borderLayer.strokeColor = UIColor.black.cgColor
borderLayer.lineWidth = lineWidth
borderLayer.path = linePath.cgPath
layer.addSublayer(borderLayer)
return layer
}
/// Adds a border to edge
func getStrikeThrough(color: UIColor, thickness: CGFloat) -> CAShapeLayer {
let border = CAShapeLayer()
border.name = "strikethrough"
border.fillColor = nil
border.opacity = 1.0
border.lineWidth = thickness
border.strokeColor = color.cgColor
let linePath = UIBezierPath()
linePath.move(to: CGPoint(x: 0, y: bounds.height))
linePath.addLine(to: CGPoint(x: bounds.width, y: 0))
border.path = linePath.cgPath
return border
}
func getMaskLayer() -> CALayer {
let mask = CALayer()
mask.backgroundColor = UIColor.white.cgColor
mask.opacity = 0.3
mask.frame = bounds
return mask
}
}

View File

@ -0,0 +1,23 @@
//
// RadioBoxCollectionViewCell.swift
// MVMCoreUI
//
// Created by Dhamodaram Nandi on 01/04/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class RadioBoxCollectionViewCell: CollectionViewCell {
let radioBox = RadioBox()
open override func setupView() {
super.setupView()
addMolecule(radioBox)
MVMCoreUIUtility.setMarginsFor(contentView, leading: 0, top: 0, trailing: 0, bottom: 0)
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
guard let model = model as? RadioBoxModel else { return }
radioBox.set(with: model, delegateObject, additionalData)
}
}

View File

@ -0,0 +1,68 @@
//
// RadioBoxModel.swift
// MVMCoreUI
//
// Created by Dhamodaram Nandi on 31/03/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objcMembers public class RadioBoxModel: MoleculeModelProtocol {
public static var identifier: String = "radioBox"
public var text: String
public var subText: String?
public var backgroundColor: Color? = Color(uiColor: .white)
public var selectedAccentColor = Color(uiColor: .mvmRed)
public var selected: Bool = false
public var enabled: Bool = true
public var strikethrough: Bool = false
public var fieldValue: String?
private enum CodingKeys: String, CodingKey {
case moleculeName
case text
case subText
case selectedAccentColor
case backgroundColor
case selected
case enabled
case strikethrough
case fieldValue
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
text = try typeContainer.decode(String.self, forKey: .text)
subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText)
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor) {
selectedAccentColor = color
}
if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) {
backgroundColor = color
}
if let isSelected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) {
selected = isSelected
}
if let isEnabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) {
enabled = isEnabled
}
if let isStrikeTrough = try typeContainer.decodeIfPresent(Bool.self, forKey: .strikethrough) {
strikethrough = isStrikeTrough
}
fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(text, forKey: .text)
try container.encodeIfPresent(subText, forKey: .subText)
try container.encode(selectedAccentColor, forKey: .selectedAccentColor)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encode(selected, forKey: .selected)
try container.encode(enabled, forKey: .enabled)
try container.encode(strikethrough, forKey: .strikethrough)
try container.encodeIfPresent(fieldValue, forKey: .fieldValue)
}
}

View File

@ -0,0 +1,143 @@
//
// RadioBoxes.swift
// MVMCoreUI
//
// Created by Dhamodaram Nandi on 31/03/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class RadioBoxes: View {
public var collectionView: CollectionView!
public var collectionViewHeight: NSLayoutConstraint!
private let boxWidth: CGFloat = 151.0
private let boxHeight: CGFloat = 64.0
private let itemSpacing: CGFloat = 8.0
private var delegateObject: MVMCoreUIDelegateObject?
/// The models for the molecules.
public var boxes: [RadioBoxModel]?
private var size: CGFloat?
open override func layoutSubviews() {
super.layoutSubviews()
// Accounts for any collection size changes
DispatchQueue.main.async {
self.collectionView.collectionViewLayout.invalidateLayout()
}
}
// MARK: - MVMCoreViewProtocol
open override func setupView() {
super.setupView()
collectionView = createCollectionView()
addSubview(collectionView)
NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
collectionViewHeight?.isActive = true
}
// MARK: - MoleculeViewProtocol
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
self.delegateObject = delegateObject
guard let radioBoxesModel = model as? RadioBoxesModel else { return }
boxes = radioBoxesModel.boxes
FormValidator.setupValidation(for: radioBoxesModel, delegate: delegateObject?.formHolderDelegate)
backgroundColor = radioBoxesModel.backgroundColor?.uiColor
registerCells()
setHeight()
collectionView.reloadData()
}
@objc override open func updateView(_ size: CGFloat) {
super.updateView(size)
self.size = size
collectionView.updateView(size)
}
// MARK: - Creation
/// Creates the layout for the collection.
open func createCollectionViewLayout() -> UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = itemSpacing
layout.minimumInteritemSpacing = itemSpacing
return layout
}
/// Creates the collection view.
open func createCollectionView() -> CollectionView {
let collection = CollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
collection.dataSource = self
collection.delegate = self
return collection
}
/// Registers the cells with the collection view
open func registerCells() {
collectionView.register(RadioBoxCollectionViewCell.self, forCellWithReuseIdentifier: "RadioBoxCollectionViewCell")
}
// MARK: - JSON Setters
open func setHeight() {
guard let boxes = boxes, boxes.count > 0 else {
collectionViewHeight.constant = 0
return
}
// Calculate the height
let rows = ceil(CGFloat(boxes.count) / 2.0)
let height = (rows * boxHeight) + ((rows - 1) * itemSpacing)
collectionViewHeight?.constant = height
}
}
extension RadioBoxes: UICollectionViewDelegateFlowLayout {
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let itemWidth: CGFloat = (collectionView.bounds.width - itemSpacing) / 2
return CGSize(width: itemWidth, height: boxHeight)
}
}
extension RadioBoxes: UICollectionViewDataSource {
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return boxes?.count ?? 0
}
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let molecule = boxes?[indexPath.row],
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RadioBoxCollectionViewCell", for: indexPath) as? RadioBoxCollectionViewCell else {
fatalError()
}
cell.radioBox.isUserInteractionEnabled = false
cell.set(with: molecule, delegateObject, nil)
cell.updateView(size ?? collectionView.bounds.width)
if molecule.selected {
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
}
cell.layoutIfNeeded()
return cell
}
}
extension RadioBoxes: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return }
cell.radioBox.selectBox()
_ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
}
public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
guard let cell = collectionView.cellForItem(at: indexPath) as? RadioBoxCollectionViewCell else { return }
cell.radioBox.deselectBox()
}
}

View File

@ -0,0 +1,57 @@
//
// RadioBoxesModel.swift
// MVMCoreUI
//
// Created by Dhamodaram Nandi on 31/03/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objcMembers public class RadioBoxesModel: MoleculeModelProtocol, FormFieldProtocol {
public static var identifier: String = "radioBoxes"
public var backgroundColor: Color?
public var selectedAccentColor: Color?
public var boxes: [RadioBoxModel]
public var fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName
public var baseValue: AnyHashable?
/// Returns the fieldValue of the selected box, otherwise the text of the selected box.
public func formFieldValue() -> AnyHashable? {
let selectedBox = boxes.first { (box) -> Bool in
return box.selected
}
return selectedBox?.fieldValue ?? selectedBox?.text
}
private enum CodingKeys: String, CodingKey {
case moleculeName
case selectedAccentColor
case backgroundColor
case boxes
case fieldKey
case groupName
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
boxes = try typeContainer.decode([RadioBoxModel].self, forKey: .boxes)
fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey)
if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) {
self.groupName = groupName
}
baseValue = formFieldValue()
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encode(boxes, forKey: .boxes)
try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(fieldKey, forKey: .fieldKey)
try container.encode(groupName, forKey: .groupName)
}
}

View File

@ -20,6 +20,12 @@ import UIKit
}
}
public override var isSelected: Bool {
didSet {
radioModel?.state = isSelected
}
}
public var enabledColor: UIColor = .mvmBlack
public var disabledColor: UIColor = .mvmCoolGray3
public var delegateObject: MVMCoreUIDelegateObject?

View File

@ -55,7 +55,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol {
//--------------------------------------------------
public func formFieldValue() -> AnyHashable? {
return state
return fieldValue
}
//--------------------------------------------------

View File

@ -17,15 +17,30 @@ import Foundation
public var fieldKey: String?
public var groupName: String = FormValidator.defaultGroupName
private var selectedRadioButton: RadioButton?
private var fieldGroupName: String?
private var selectedRadioButtonModel: RadioButtonModel?
public var baseValue: AnyHashable?
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
init(_ fieldKey: String?) {
self.fieldKey = fieldKey
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public func set(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton) {
self.fieldKey = radioButtonModel.fieldKey
self.groupName = radioButtonModel.groupName
if radioButtonModel.state {
if self.baseValue == nil,
let selected = radioButtonModel.baseValue as? Bool, selected {
self.baseValue = radioButtonModel.fieldValue
}
selectedRadioButtonModel = radioButtonModel
// Below code is needed for cell resuse scenario.
radioButton.isSelected = true
selectedRadioButton = radioButton
} else {
radioButton.isSelected = false
}
}
//--------------------------------------------------
@ -35,35 +50,33 @@ import Foundation
public static func setupForRadioButtonGroup(_ radioButtonModel: RadioButtonModel, _ radioButton: RadioButton, delegateObject: MVMCoreUIDelegateObject?) {
guard let groupName = radioButtonModel.fieldKey,
let formValidator = delegateObject?.formHolderDelegate?.formValidator
else { return }
let radioButtonSelectionHelper = formValidator.radioButtonsModelByGroup[groupName] ?? RadioButtonSelectionHelper(radioButtonModel.fieldKey)
radioButtonSelectionHelper.fieldGroupName = radioButtonModel.fieldKey
formValidator.radioButtonsModelByGroup[groupName] = radioButtonSelectionHelper
if radioButtonModel.state {
radioButtonSelectionHelper.selectedRadioButton = radioButton
let formValidator = delegateObject?.formHolderDelegate?.formValidator else {
return
}
let radioButtonSelectionHelper = formValidator.radioButtonsModelByGroup[groupName] ?? RadioButtonSelectionHelper()
radioButtonSelectionHelper.set(radioButtonModel, radioButton)
formValidator.radioButtonsModelByGroup[groupName] = radioButtonSelectionHelper
FormValidator.setupValidation(for: radioButtonSelectionHelper, delegate: delegateObject?.formHolderDelegate)
}
public func selected(_ radioButton: RadioButton) {
selectedRadioButton?.isSelected = false
// Checks because the view could be reused
if selectedRadioButton?.radioModel === selectedRadioButtonModel {
selectedRadioButton?.isSelected = false
} else {
selectedRadioButtonModel?.state = false
}
selectedRadioButton = radioButton
selectedRadioButton?.isSelected = true
selectedRadioButtonModel = selectedRadioButton?.radioModel
}
}
// MARK: - FormValidationFormFieldProtocol
extension RadioButtonSelectionHelper {
public func formFieldGroupName() -> String? {
return selectedRadioButton?.formFieldGroupName() ?? fieldGroupName
}
public func formFieldValue() -> AnyHashable? {
return selectedRadioButton?.formFieldValue()
return selectedRadioButtonModel?.fieldValue
}
}

View File

@ -47,7 +47,7 @@ import Foundation
// need to move labelattributemodel to different method
try? ModelRegistry.register(LabelAttributeFontModel.self)
try? ModelRegistry.register(LabelAttributeColorModel.self)
try? ModelRegistry.register(LabelAttributeImageModel.self) // We need to separate the registry by types due to collisions...
try? ModelRegistry.register(LabelAttributeImageModel.self)
try? ModelRegistry.register(LabelAttributeUnderlineModel.self)
try? ModelRegistry.register(LabelAttributeStrikeThroughModel.self)
try? ModelRegistry.register(LabelAttributeActionModel.self)
@ -66,6 +66,11 @@ import Foundation
MoleculeObjectMapping.shared()?.register(viewClass: ItemDropdownEntryField.self, viewModelClass: ItemDropdownEntryFieldModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: DateDropdownEntryField.self, viewModelClass: DateDropdownEntryFieldModel.self)
// Selectors
MoleculeObjectMapping.shared()?.register(viewClass: RadioButton.self, viewModelClass: RadioButtonModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: RadioBoxes.self, viewModelClass: RadioBoxesModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: Checkbox.self, viewModelClass: CheckboxModel.self)
// Other Atoms
MoleculeObjectMapping.shared()?.register(viewClass: ProgressBar.self, viewModelClass: ProgressBarModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: MultiProgress.self, viewModelClass: MultiProgressBarModel.self)
@ -75,10 +80,8 @@ import Foundation
MoleculeObjectMapping.shared()?.register(viewClass: Line.self, viewModelClass: LineModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: GraphView.self, viewModelClass: CircleProgressModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: Toggle.self, viewModelClass: ToggleModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: Checkbox.self, viewModelClass: CheckboxModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: CheckboxLabel.self, viewModelClass: CheckboxLabelModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: Arrow.self, viewModelClass: ArrowModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: RadioButton.self, viewModelClass: RadioButtonModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: RadioButtonLabel.self, viewModelClass: RadioButtonLabelModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: WebView.self, viewModelClass: WebViewModel.self)
@ -112,7 +115,9 @@ import Foundation
// Other Items
MoleculeObjectMapping.shared()?.register(viewClass: MoleculeStackItem.self, viewModelClass: MoleculeStackItemModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: StackItem.self, viewModelClass: StackItemModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: MoleculeCollectionViewCell.self, viewModelClass: CarouselItemModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: MoleculeCollectionViewCell.self, viewModelClass: MoleculeCollectionItemModel.self)
MoleculeObjectMapping.shared()?.register(viewClass: CarouselItem.self, viewModelClass: CarouselItemModel.self)
// Other Container Molecules
MoleculeObjectMapping.shared()?.register(viewClass: MoleculeHeaderView.self, viewModelClass: MoleculeHeaderModel.self)

View File

@ -0,0 +1,78 @@
//
// CarouselItem.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class CarouselItem: MoleculeCollectionViewCell {
open var allowsPeaking = false
var peakingLeftArrow = UIImageView(image: MVMCoreUIUtility.imageNamed("peakingRightArrow")?.withRenderingMode(.alwaysTemplate))
var peakingRightArrow = UIImageView(image: MVMCoreUIUtility.imageNamed("peakingRightArrow")?.withRenderingMode(.alwaysTemplate))
var peakingCover = MVMCoreUICommonViewsUtility.commonView()
open override func addMolecule(_ molecule: MoleculeViewProtocol) {
super.addMolecule(molecule)
contentView.sendSubviewToBack(molecule)
}
open override func setupView() {
super.setupView()
// Covers the card when peaking.
peakingCover.backgroundColor = .white
peakingCover.alpha = 0
contentView.addSubview(peakingCover)
NSLayoutConstraint.constraintPinSubview(toSuperview: peakingCover)
// A small arrow on the next card for when peaking.
let ratio: CGFloat = 0.015
peakingLeftArrow.translatesAutoresizingMaskIntoConstraints = false
peakingLeftArrow.alpha = 0
peakingLeftArrow.tintColor = .black
contentView.addSubview(peakingLeftArrow)
peakingLeftArrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
NSLayoutConstraint.scalingPinViewLeft(toSuper: peakingLeftArrow, ratio: ratio, anchor: contentView.widthAnchor)
peakingRightArrow.translatesAutoresizingMaskIntoConstraints = false
peakingRightArrow.transform = CGAffineTransform(scaleX: -1, y: 1) // Flip
peakingRightArrow.alpha = 0
peakingRightArrow.tintColor = .black
contentView.addSubview(peakingRightArrow)
peakingRightArrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
NSLayoutConstraint.scalingPinViewRight(toSuper: peakingRightArrow, ratio: ratio, anchor: contentView.widthAnchor)
}
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let collectionModel = model as? CarouselItemModel else { return }
// Handles peaking.
allowsPeaking = collectionModel.peakingUI ?? false
if let peakingArrowColor = collectionModel.peakingArrowColor {
let color = peakingArrowColor.uiColor
peakingLeftArrow.tintColor = color
peakingRightArrow.tintColor = color
}
}
public func setPeaking(_ peaking: Bool, animated: Bool) {
guard allowsPeaking else {
return
}
let animation = {() in
self.peakingRightArrow.alpha = peaking ? 1 : 0
self.peakingLeftArrow.alpha = peaking ? 1 : 0
self.peakingCover.alpha = peaking ? 0.5 : 0
}
if animated {
UIView.animate(withDuration: 0.4, animations: animation)
} else {
animation()
}
}
}

View File

@ -9,22 +9,20 @@
import Foundation
@objcMembers public class CarouselItemModel: MoleculeContainerModel, CarouselItemModelProtocol {
public static var identifier: String = "carouselItem"
public var backgroundColor: Color?
@objcMembers public class CarouselItemModel: MoleculeCollectionItemModel, CarouselItemModelProtocol {
public override class var identifier: String {
return "carouselItem"
}
public var peakingUI: Bool?
public var peakingArrowColor: Color?
private enum CodingKeys: String, CodingKey {
case moleculeName
case backgroundColor
case peakingUI
case peakingArrowColor
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
peakingUI = try typeContainer.decodeIfPresent(Bool.self, forKey: .peakingUI)
peakingArrowColor = try typeContainer.decodeIfPresent(Color.self, forKey: .peakingArrowColor)
try super.init(from: decoder)
@ -33,8 +31,6 @@ import Foundation
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
try container.encodeIfPresent(peakingUI, forKey: .peakingUI)
try container.encodeIfPresent(peakingArrowColor, forKey: .peakingArrowColor)
}

View File

@ -0,0 +1,57 @@
//
// MoleculeCollectionItemModel.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
/// A model for a collection item that is a container for any molecule.
@objcMembers public class MoleculeCollectionItemModel: MoleculeContainerModel, CollectionItemModelProtocol, MoleculeModelProtocol {
open class var identifier: String {
return "collectionItem"
}
public var backgroundColor: Color?
/// Defaults to set
public func setDefaults() {
if useHorizontalMargins == nil {
useHorizontalMargins = true
}
if useVerticalMargins == nil {
useVerticalMargins = true
}
if topMarginPadding == nil {
topMarginPadding = PaddingDefault
}
if bottomMarginPadding == nil {
bottomMarginPadding = PaddingDefault
}
}
private enum CodingKeys: String, CodingKey {
case moleculeName
case backgroundColor
}
public override init(with moleculeModel: MoleculeModelProtocol) {
super.init(with: moleculeModel)
setDefaults()
}
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
try super.init(from: decoder)
setDefaults()
}
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(moleculeName, forKey: .moleculeName)
try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor)
}
}

View File

@ -8,90 +8,15 @@
import UIKit
open class MoleculeCollectionViewCell: UICollectionViewCell, MoleculeViewProtocol {
open var molecule: MoleculeViewProtocol?
public let containerHelper = ContainerHelper()
// In updateView, will set padding to default.
open var updateViewHorizontalDefaults = true
open var updateViewVerticalDefaults = true
open var allowsPeaking = false
var peakingLeftArrow = UIImageView(image: MVMCoreUIUtility.imageNamed("peakingRightArrow")?.withRenderingMode(.alwaysTemplate))
var peakingRightArrow = UIImageView(image: MVMCoreUIUtility.imageNamed("peakingRightArrow")?.withRenderingMode(.alwaysTemplate))
var peakingCover = MVMCoreUICommonViewsUtility.commonView()
public override init(frame: CGRect) {
super.init(frame: .zero)
setupView()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
public func setupView() {
guard peakingCover.superview == nil else {
return
}
isAccessibilityElement = false
contentView.isAccessibilityElement = false
insetsLayoutMarginsFromSafeArea = false
contentView.insetsLayoutMarginsFromSafeArea = false
contentView.preservesSuperviewLayoutMargins = false
// Covers the card when peaking.
peakingCover.backgroundColor = .white
peakingCover.alpha = 0
contentView.addSubview(peakingCover)
NSLayoutConstraint.constraintPinSubview(toSuperview: peakingCover)
// A small arrow on the next card for when peaking.
let ratio: CGFloat = 0.015
peakingLeftArrow.translatesAutoresizingMaskIntoConstraints = false
peakingLeftArrow.alpha = 0
peakingLeftArrow.tintColor = .black
contentView.addSubview(peakingLeftArrow)
peakingLeftArrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
NSLayoutConstraint.scalingPinViewLeft(toSuper: peakingLeftArrow, ratio: ratio, anchor: contentView.widthAnchor)
peakingRightArrow.translatesAutoresizingMaskIntoConstraints = false
peakingRightArrow.transform = CGAffineTransform(scaleX: -1, y: 1) // Flip
peakingRightArrow.alpha = 0
peakingRightArrow.tintColor = .black
contentView.addSubview(peakingRightArrow)
peakingRightArrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
NSLayoutConstraint.scalingPinViewRight(toSuper: peakingRightArrow, ratio: ratio, anchor: contentView.widthAnchor)
}
public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
guard let collectionModel = model as? CarouselItemModel else { return }
if let useHorizontalMargins = collectionModel.useHorizontalMargins {
updateViewHorizontalDefaults = useHorizontalMargins
}
if let useVerticalMargins = collectionModel.useVerticalMargins {
updateViewVerticalDefaults = useVerticalMargins
}
// Handles peaking.
allowsPeaking = collectionModel.peakingUI ?? false
if let peakingArrowColor = collectionModel.peakingArrowColor {
let color = peakingArrowColor.uiColor
peakingLeftArrow.tintColor = color
peakingRightArrow.tintColor = color
}
if let backgroundColor = collectionModel.backgroundColor {
self.backgroundColor = backgroundColor.uiColor
}
/// A collection item that is a container for any molecule.
open class MoleculeCollectionViewCell: CollectionViewCell {
open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
super.set(with: model, delegateObject, additionalData)
guard let collectionModel = model as? MoleculeCollectionItemModel else { return }
if molecule == nil {
if let moleculeView = MoleculeObjectMapping.shared()?.createMolecule(collectionModel.molecule, delegateObject: delegateObject, additionalData: additionalData) {
contentView.insertSubview(moleculeView, at: 0)
containerHelper.constrainView(moleculeView)
molecule = moleculeView
addMolecule(moleculeView)
}
} else {
molecule?.set(with: collectionModel.molecule, delegateObject, additionalData)
@ -102,39 +27,33 @@ open class MoleculeCollectionViewCell: UICollectionViewCell, MoleculeViewProtoco
accessibilityElements = molecule.subviews
}
public func reset() {
open override func reset() {
super.reset()
molecule?.reset()
updateViewVerticalDefaults = true
updateViewHorizontalDefaults = true
backgroundColor = .white
}
public class func nameForReuse(_ model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
guard let molecule = (model as? CarouselItemModel)?.molecule,
let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(molecule) else {
return nil
}
return moleculeClass.nameForReuse(with: molecule, delegateObject) ?? molecule.moleculeName
open class func nameForReuse(_ model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? {
guard let moleculeModel = (model as? MoleculeCollectionItemModel)?.molecule else { return "\(self)<>" }
let className = MoleculeObjectMapping.shared()?.getMoleculeClass(moleculeModel)
let moleculeName = className?.nameForReuse(with: moleculeModel, delegateObject) ?? moleculeModel.moleculeName
return "\(self)<\(moleculeName)>"
}
public func updateView(_ size: CGFloat) {
(molecule as? MVMCoreViewProtocol)?.updateView(size)
MFStyler.setDefaultMarginsFor(contentView, size: size, horizontal: updateViewHorizontalDefaults, vertical: updateViewVerticalDefaults)
open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject?>?) -> [String]? {
guard let moleculeModel = (model as? MoleculeCollectionItemModel)?.molecule,
let theClass = MoleculeObjectMapping.shared()?.getMoleculeClass(moleculeModel)
else { return nil }
return theClass.requiredModules(with: moleculeModel, delegateObject, error: error)
}
public func setPeaking(_ peaking: Bool, animated: Bool) {
guard allowsPeaking else {
return
}
let animation = {() in
self.peakingRightArrow.alpha = peaking ? 1 : 0
self.peakingLeftArrow.alpha = peaking ? 1 : 0
self.peakingCover.alpha = peaking ? 0.5 : 0
}
if animated {
UIView.animate(withDuration: 0.4, animations: animation)
} else {
animation()
}
open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat {
guard let model = model as? MoleculeCollectionItemModel,
let classType = MoleculeObjectMapping.shared()?.getMoleculeClass(model.molecule),
let height = classType.estimatedHeight(with: model.molecule, delegateObject)
else { return 100 }
return height + (model.topMarginPadding ?? 0) + (model.bottomMarginPadding ?? 0)
}
}

View File

@ -10,7 +10,7 @@ import UIKit
open class Carousel: View {
public let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
public let collectionView = CollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
/// The current index of the collection view. Includes dummy cells when looping.
public var currentIndex = 0
@ -52,27 +52,23 @@ open class Carousel: View {
public var delegateObject: MVMCoreUIDelegateObject?
// MARK: - MVMCoreViewProtocol
open override func setupView() {
super.setupView()
guard collectionView.superview == nil else {
return
}
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.dataSource = self
collectionView.delegate = self
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = .clear
collectionView.isAccessibilityElement = false
addSubview(collectionView)
bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
collectionViewHeight?.isActive = false
private var size: CGFloat?
// Updates the model and index.
public func updateModelIndex() {
(model as? CarouselModel)?.index = pageIndex
}
open override func updateView(_ size: CGFloat) {
super.updateView(size)
open override func layoutSubviews() {
super.layoutSubviews()
// Accounts for any collection size changes
DispatchQueue.main.async {
self.layoutCollection()
}
}
/// Invalidates the layout and ensures we are paged to the correct cell.
open func layoutCollection() {
collectionView.collectionViewLayout.invalidateLayout()
showPeaking(false)
@ -84,6 +80,29 @@ open class Carousel: View {
}
}
// MARK: - MVMCoreViewProtocol
open override func setupView() {
super.setupView()
collectionView.dataSource = self
collectionView.delegate = self
addSubview(collectionView)
bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint
collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
collectionViewHeight?.isActive = false
}
open override func updateView(_ size: CGFloat) {
super.updateView(size)
self.size = size
// Update cells and re-layout.
for cell in collectionView.visibleCells {
(cell as? MVMCoreViewProtocol)?.updateView(size)
}
layoutCollection()
}
// MARK: - MoleculeViewProtocol
public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
self.delegateObject = delegateObject
@ -108,6 +127,9 @@ open class Carousel: View {
}
setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject)
pageIndex = carouselModel.index
pagingView?.setPage(carouselModel.index)
collectionView.reloadData()
}
@ -143,7 +165,7 @@ open class Carousel: View {
}
/// Sets up the paging molecule
open func setupPagingMolecule(_ molecule: CarouselPagingModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) {
open func setupPagingMolecule(_ molecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?, delegateObject: MVMCoreUIDelegateObject?) {
var pagingView: (UIView & MVMCoreUIPagingProtocol)? = nil
if let molecule = molecule {
pagingView = MoleculeObjectMapping.shared()?.createMolecule(molecule, delegateObject: delegateObject) as? (UIView & MVMCoreUIPagingProtocol)
@ -197,6 +219,7 @@ open class Carousel: View {
}
let currentPage = pager.currentPage()
localSelf.pageIndex = currentPage
localSelf.updateModelIndex()
localSelf.goTo(localSelf.currentIndex, animated: !UIAccessibility.isVoiceOverRunning)
})
}
@ -208,15 +231,15 @@ open class Carousel: View {
// Show overlay and arrow in peaking Cell
let visibleItemsPaths = collectionView.indexPathsForVisibleItems.sorted { $0.row < $1.row }
if let firstItem = visibleItemsPaths.first, firstItem.row != currentIndex {
(collectionView.cellForItem(at: firstItem) as? MoleculeCollectionViewCell)?.setPeaking(true, animated: true)
(collectionView.cellForItem(at: firstItem) as? CarouselItem)?.setPeaking(true, animated: true)
}
if let lastItem = visibleItemsPaths.last, lastItem.row != currentIndex {
(collectionView.cellForItem(at: lastItem) as? MoleculeCollectionViewCell)?.setPeaking(true, animated: true)
(collectionView.cellForItem(at: lastItem) as? CarouselItem)?.setPeaking(true, animated: true)
}
} else {
// Hide peaking.
for item in collectionView.visibleCells {
(item as? MoleculeCollectionViewCell)?.setPeaking(false, animated: true)
(item as? CarouselItem)?.setPeaking(false, animated: true)
}
}
}
@ -229,10 +252,12 @@ open class Carousel: View {
cell.accessibilityElementsHidden = false
var array = cell.accessibilityElements
if let acc = pagingView?.accessibilityElements {
array?.append(contentsOf: acc)
} else {
array?.append(pagingView!)
if let pagingView = pagingView {
if let acc = pagingView.accessibilityElements {
array?.append(contentsOf: acc)
} else {
array?.append(pagingView)
}
}
self.accessibilityElements = array
@ -249,7 +274,7 @@ extension Carousel: UICollectionViewDelegateFlowLayout {
}
open func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
(cell as? MoleculeCollectionViewCell)?.setPeaking(false, animated: false)
(cell as? CarouselItem)?.setPeaking(false, animated: false)
}
}
@ -268,7 +293,7 @@ extension Carousel: UICollectionViewDataSource {
protocolCell.reset()
protocolCell.set(with: moleculeInfo.molecule, delegateObject, nil)
}
(cell as? MVMCoreViewProtocol)?.updateView(collectionView.bounds.width)
(cell as? MVMCoreViewProtocol)?.updateView(size ?? collectionView.bounds.width)
setAccessiblity(cell, index: indexPath.row)
return cell
}
@ -278,19 +303,18 @@ extension Carousel: UIScrollViewDelegate {
func goTo(_ index: Int, animated: Bool) {
showPeaking(false)
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)), index: index)
self.currentIndex = index
self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.itemAlignment, animated: animated)
if let cell = collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)) {
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: self.currentIndex, section: 0)), index: index)
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
currentIndex = index
updateModelIndex()
collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: itemAlignment, animated: animated)
if let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) {
setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
UIAccessibility.post(notification: .layoutChanged, argument: cell)
}
}
func handleUserOnBufferCell() {
guard loop else {
return
}
guard loop else { return }
let lastPageIndex = numberOfPages + 1
let goToIndex = {(index: Int) in
@ -318,9 +342,11 @@ extension Carousel: UIScrollViewDelegate {
let index = scrollView.contentOffset.x / (itemWidth + separatorWidth)
let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
if index < 1 {
self.currentIndex = 0
currentIndex = 0
updateModelIndex()
} else if index > CGFloat(lastCellIndex - 1) {
self.currentIndex = lastCellIndex
currentIndex = lastCellIndex
updateModelIndex()
}
}
@ -330,7 +356,7 @@ extension Carousel: UIScrollViewDelegate {
open func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Check if the user is dragging the card even further past the next card.
checkForDraggingOutOfBounds(scrollView)
//checkForDraggingOutOfBounds(scrollView)
// Let the pager know our progress if needed.
pagingView?.scrollViewDidScroll?(collectionView)
@ -346,9 +372,7 @@ extension Carousel: UIScrollViewDelegate {
targetContentOffset.pointee = scrollView.contentOffset
// This is for setting up smooth custom paging. (Since UICollectionView only handles paging based on collection view size and not cell size).
guard let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing else {
return
}
guard let separatorWidth = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing else { return }
// We switch cards if we pass the velocity threshold or position threshold (currently 50%).
let itemWidth = collectionView.bounds.width * CGFloat(itemWidthPercent)

View File

@ -12,14 +12,14 @@ import UIKit
public static var identifier: String = "carousel"
public var backgroundColor: Color?
public var molecules: [CarouselItemModel]
public var index: Int = 0
public var spacing: Float?
public var border: Bool?
public var loop: Bool?
public var height: Float?
public var itemWidthPercent: Float?
public var itemAlignment: UICollectionView.ScrollPosition?
public var pagingMolecule: CarouselPagingModelProtocol?
public var pagingMolecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?
public init(molecules: [CarouselItemModel]){
self.molecules = molecules
@ -29,6 +29,7 @@ import UIKit
case moleculeName
case backgroundColor
case molecules
case index
case spacing
case border
case loop
@ -40,15 +41,16 @@ import UIKit
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
self.molecules = try typeContainer.decode([CarouselItemModel].self, forKey: .molecules)
self.backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
self.spacing = try typeContainer.decode(Float.self, forKey: .spacing)
self.border = try typeContainer.decode(Bool.self, forKey: .border)
self.loop = try typeContainer.decode(Bool.self, forKey: .loop)
self.height = try typeContainer.decode(Float.self, forKey: .height)
self.itemWidthPercent = try typeContainer.decode(Float.self, forKey: .itemWidthPercent)
self.itemAlignment = try typeContainer.decode(UICollectionView.ScrollPosition.self, forKey: .itemAlignment)
self.pagingMolecule = try typeContainer.decodeModelIfPresent(codingKey: .pagingMolecule)
molecules = try typeContainer.decode([CarouselItemModel].self, forKey: .molecules)
index = try typeContainer.decodeIfPresent(Int.self, forKey: .index) ?? 0
backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor)
spacing = try typeContainer.decodeIfPresent(Float.self, forKey: .spacing)
border = try typeContainer.decodeIfPresent(Bool.self, forKey: .border)
loop = try typeContainer.decodeIfPresent(Bool.self, forKey: .loop)
height = try typeContainer.decodeIfPresent(Float.self, forKey: .height)
itemWidthPercent = try typeContainer.decodeIfPresent(Float.self, forKey: .itemWidthPercent)
itemAlignment = try typeContainer.decodeIfPresent(UICollectionView.ScrollPosition.self, forKey: .itemAlignment)
pagingMolecule = try typeContainer.decodeModelIfPresent(codingKey: .pagingMolecule)
}
public func encode(to encoder: Encoder) throws {

View File

@ -9,6 +9,6 @@
import Foundation
public protocol CarouselPagingModelProtocol: MoleculeModelProtocol {
public protocol CarouselPagingModelProtocol {
var position: Float? { get }
}

View File

@ -0,0 +1,195 @@
//
// CollectionTemplate.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objc open class CollectionTemplate: ThreeLayerCollectionViewController, TemplateProtocol {
public typealias TemplateModel = CollectionTemplateModel
public var templateModel: CollectionTemplateModel?
public var moleculesInfo: [(identifier: String, class: AnyClass, molecule: (CollectionItemModelProtocol & MoleculeModelProtocol))]?
var observer: NSKeyValueObservation?
//--------------------------------------------------
// MARK: - Computed Properties
//--------------------------------------------------
open override func parsePageJSON() throws {
try parseTemplate(json: loadObject?.pageJSON)
try super.parsePageJSON()
}
open override var loadObject: MVMCoreLoadObject? {
didSet {
guard loadObject != oldValue else { return }
updateRequiredModules()
observer?.invalidate()
if let newObject = loadObject {
observer = newObject.observe(\MVMCoreLoadObject.pageJSON, options: [.old, .new]) { [weak self] object, change in
self?.updateRequiredModules()
}
}
}
}
//--------------------------------------------------
// MARK: - Methods
//--------------------------------------------------
open override func viewForTop() -> UIView? {
guard let headerModel = templateModel?.header,
let molecule = MoleculeObjectMapping.shared()?.createMolecule(headerModel, delegateObject: delegateObjectIVar)
else { return super.viewForTop() }
// Temporary, Default the horizontal padding
if var container = templateModel?.header as? ContainerModelProtocol, container.useHorizontalMargins == nil {
container.useHorizontalMargins = true
}
return molecule
}
override open func viewForBottom() -> UIView? {
guard let footerModel = templateModel?.footer,
let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar)
else { return super.viewForBottom() }
return molecule
}
open override func shouldFinishProcessingLoad(_ loadObject: MVMCoreLoadObject, error: AutoreleasingUnsafeMutablePointer<MVMCoreErrorObject>) -> Bool {
guard super.shouldFinishProcessingLoad(loadObject, error: error) else { return false }
// This template requires atleast one of the three layers.
if templateModel?.header == nil,
templateModel?.molecules?.count ?? 0 == 0,
templateModel?.footer == nil,
let errorObject = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess), messageToLog: "Collection template requires atleast one of the following: header, footer, molecules", code: CoreUIErrorCode.ErrorCodeListMolecule.rawValue, domain: ErrorDomainNative, location: String(describing: self)) {
error.pointee = errorObject
return false
}
return true
}
open override func handleNewData() {
setup()
registerCells()
super.handleNewData()
}
//--------------------------------------------------
// MARK: - Collection
//--------------------------------------------------
open override func update(cell: UICollectionViewCell, size: CGFloat) {
super.update(cell: cell, size: size)
// Update the width for columns.
if let collectionView = collectionView,
let columns = templateModel?.columns, columns > 0,
let cell = cell as? CollectionTemplateItemProtocol {
let width = (size - collectionView.adjustedContentInset.left - collectionView.adjustedContentInset.right) / CGFloat(columns)
cell.set(width: width)
}
}
open override func registerCells() {
super.registerCells()
guard let moleculesInfo = moleculesInfo else { return }
for moleculeInfo in moleculesInfo {
collectionView?.register(moleculeInfo.class, forCellWithReuseIdentifier: moleculeInfo.identifier)
}
}
open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return moleculesInfo?.count ?? 0
}
open override func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
open override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let moleculeInfo = moleculesInfo?[indexPath.row]
else { return UICollectionViewCell() }
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: moleculeInfo.identifier, for: indexPath)
(cell as? MoleculeViewProtocol)?.reset()
(cell as? MoleculeViewProtocol)?.set(with: moleculeInfo.molecule, delegateObjectIVar, nil)
update(cell: cell, size: view.frame.width)
// Neded to fix an apple defect where the cell is not the correct size on certain devices for certain cells
cell.layoutIfNeeded()
return cell
}
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
(collectionView.cellForItem(at: indexPath) as? CollectionTemplateItemProtocol)?.didSelectCell(at: indexPath, delegateObject: delegateObjectIVar, additionalData: nil)
}
public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
(cell as? CollectionTemplateItemProtocol)?.willDisplay()
}
//--------------------------------------------------
// MARK: - Convenience
//--------------------------------------------------
/// Returns the (identifier, class) of the molecule for the given map.
open func getMoleculeInfo(with item: (CollectionItemModelProtocol & MoleculeModelProtocol)?) -> (identifier: String, class: AnyClass, molecule: CollectionItemModelProtocol & MoleculeModelProtocol)? {
guard let item = item,
let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(item) else { return nil }
let moleculeName = moleculeClass.nameForReuse(with: item, delegateObjectIVar) ?? item.moleculeName
return (moleculeName, moleculeClass, item)
}
/// Sets up the molecule list and ensures no errors loading all content.
open func getMoleculeInfoList() -> [(identifier: String, class: AnyClass, molecule: (CollectionItemModelProtocol & MoleculeModelProtocol))]? {
var moleculeList: [(identifier: String, class: AnyClass, molecule: CollectionItemModelProtocol & MoleculeModelProtocol)] = []
if let molecules = templateModel?.molecules {
for molecule in molecules {
if let info = getMoleculeInfo(with: molecule) {
moleculeList.append(info)
}
}
}
return moleculeList.count > 0 ? moleculeList : nil
}
/// Sets up the header, footer, molecule list and ensures no errors loading all content.
open func setup() {
moleculesInfo = getMoleculeInfoList()
}
/// Adds modules from requiredModules() to the MVMCoreViewControllerMapping.requiredModules map.
open func updateRequiredModules() {
if let requiredModules = requiredModules(), let pageType = pageType {
MVMCoreViewControllerMappingObject.shared()?.addRequiredModules(toMapping: requiredModules, forPageType: pageType)
}
}
/// Gets modules required by the loadObject.pageJSON.
open func requiredModules() -> [Any]? {
var modules: [String]? = []
var errors: [MVMCoreErrorObject]? = nil
MoleculeObjectMapping.addRequiredModules(for: templateModel?.header, delegateObjectIVar, moduleList: &modules, errorList: &errors)
MoleculeObjectMapping.addRequiredModules(for: templateModel?.footer, delegateObjectIVar, moduleList: &modules, errorList: &errors)
if let molecules = templateModel?.molecules {
for molecule in molecules {
MoleculeObjectMapping.addRequiredModules(for: molecule, delegateObjectIVar, moduleList: &modules, errorList: &errors)
}
}
return modules
}
}

View File

@ -0,0 +1,29 @@
//
// CollectionTemplateItemProtocol.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/9/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
/// A protocol that items of the CollectionTemplate must conform to.
public protocol CollectionTemplateItemProtocol: UICollectionViewCell {
/// Set the width of the item. Used for the columns functionality
func set(width: CGFloat)
/// Handle action when cell is pressed
func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?)
/// Called when the cell will display.
func willDisplay()
}
// Default implementation does nothing
extension CollectionTemplateItemProtocol {
public func didSelectCell(at index: IndexPath, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) {}
public func willDisplay() {}
}

View File

@ -0,0 +1,67 @@
//
// CollectionTemplateModel.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
@objcMembers public class CollectionTemplateModel: TemplateModel {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public override class var identifier: String {
return "collection"
}
public var header: MoleculeModelProtocol?
public var molecules: [CollectionItemModelProtocol & MoleculeModelProtocol]?
public var footer: MoleculeModelProtocol?
public var columns: Int?
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
public init(pageType: String, screenHeading: String?, molecules: [CollectionItemModelProtocol & MoleculeModelProtocol]) {
super.init(pageType: pageType)
self.screenHeading = screenHeading
self.molecules = molecules
}
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case molecules
case header
case footer
case columns
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
molecules = try typeContainer.decodeModelsIfPresent(codingKey: .molecules)
header = try typeContainer.decodeModelIfPresent(codingKey: .header)
footer = try typeContainer.decodeModelIfPresent(codingKey: .footer)
columns = try typeContainer.decodeIfPresent(Int.self, forKey: .columns)
try super.init(from: decoder)
}
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeModelsIfPresent(molecules, forKey: .molecules)
try container.encodeModelIfPresent(header, forKey: .header)
try container.encodeModelIfPresent(footer, forKey: .footer)
try container.encodeIfPresent(columns, forKey: .columns)
}
}

View File

@ -81,9 +81,9 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol
}
open override func handleNewData() {
super.handleNewData()
setup()
registerWithTable()
super.handleNewData()
}
//--------------------------------------------------

View File

@ -51,10 +51,6 @@ import UIKit
return molecule
}
open override func spaceBetweenMiddleAndBottom() -> CGFloat? {
return 0
}
open override func spaceBetweenTopAndMiddle() -> CGFloat? {
return 0
}

View File

@ -0,0 +1,47 @@
//
// CollectionView.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/10/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
open class CollectionView: UICollectionView, MVMCoreViewProtocol {
private var initialSetupPerformed = false
private func initialSetup() {
if !initialSetupPerformed {
initialSetupPerformed = true
setupView()
}
}
public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
initialSetup()
}
required public init?(coder: NSCoder) {
super.init(coder: coder)
initialSetup()
}
public func updateView(_ size: CGFloat) {
for cell in visibleCells {
(cell as? MVMCoreViewProtocol)?.updateView(size)
}
collectionViewLayout.invalidateLayout()
}
public func setupView() {
translatesAutoresizingMaskIntoConstraints = false
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
backgroundColor = .clear
isAccessibilityElement = false
contentInsetAdjustmentBehavior = .always
}
}

View File

@ -0,0 +1,104 @@
//
// CollectionViewCell.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
/// A base collection view cell with basic mvm functionality.
open class CollectionViewCell: UICollectionViewCell, MoleculeViewProtocol, MVMCoreViewProtocol, CollectionTemplateItemProtocol {
// Convenience helpers
open var molecule: MoleculeViewProtocol?
public let containerHelper = ContainerHelper()
open var model: CollectionItemModelProtocol?
/// The width, used for establishing columns
open var width: CGFloat?
private var initialSetupPerformed = false
// MARK: - Inits
public override init(frame: CGRect) {
super.init(frame: .zero)
initialSetup()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialSetup()
}
private func initialSetup() {
if !initialSetupPerformed {
initialSetupPerformed = true
setupView()
}
}
// MARK: - MVMCoreViewProtocol
open func setupView() {
isAccessibilityElement = false
contentView.isAccessibilityElement = false
insetsLayoutMarginsFromSafeArea = false
contentView.insetsLayoutMarginsFromSafeArea = false
contentView.preservesSuperviewLayoutMargins = false
MVMCoreUIUtility.setMarginsFor(self, leading: 0, top: 0, trailing: 0, bottom: 0)
}
open func updateView(_ size: CGFloat) {
if let model = model as? ContainerModelProtocol {
containerHelper.updateViewMargins(contentView, model: model, size: size)
}
(molecule as? MVMCoreViewProtocol)?.updateView(size)
}
open func reset() {
molecule?.reset()
backgroundColor = .white
width = nil
}
// MARK: - MoleculeViewProtocol
open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
guard let model = model as? CollectionItemModelProtocol else { return }
self.model = model
if let moleculeModel = model as? MoleculeModelProtocol,
let backgroundColor = moleculeModel.backgroundColor {
self.backgroundColor = backgroundColor.uiColor
}
// align if needed.
if let model = model as? ContainerModelProtocol {
containerHelper.set(with: model, for: molecule as? MVMCoreUIViewConstrainingProtocol)
}
}
/// Convenience function. Adds a molecule to the view.
open func addMolecule(_ molecule: MoleculeViewProtocol) {
contentView.addSubview(molecule)
containerHelper.constrainView(molecule)
self.molecule = molecule
}
// MARK: - CollectionTemplateItemProtocol
public func set(width: CGFloat) {
self.width = width
}
// Column logic, set width.
override open func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
guard let width = width else { return autoLayoutAttributes }
let targetSize = CGSize(width: width, height: 0)
let newSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
let newFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: newSize)
autoLayoutAttributes.frame = newFrame
return autoLayoutAttributes
}
}

View File

@ -0,0 +1,13 @@
//
// CollectionItemModelProtocol.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
public protocol CollectionItemModelProtocol {
}

View File

@ -0,0 +1,30 @@
//
// ContainerCollectionReusableView.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/7/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
/// CollectionReusableView that can contains any other view.
public class ContainerCollectionReusableView: UICollectionReusableView {
public var view: UIView?
public var topConstraint: NSLayoutConstraint?
public var bottomConstraint: NSLayoutConstraint?
public func addAndContain(view: UIView) {
guard self.view != view else { return }
self.view?.removeFromSuperview()
view.setContentCompressionResistancePriority(.required, for: .vertical)
addSubview(view)
self.view = view
topConstraint = view.topAnchor.constraint(equalTo: topAnchor)
topConstraint?.isActive = true
bottomConstraint = bottomAnchor.constraint(equalTo: view.bottomAnchor)
bottomConstraint?.isActive = true
rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
view.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
}
}

View File

@ -0,0 +1,76 @@
//
// ProgrammaticCollectionViewController.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/8/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
/// A base view controller with a collection view.
@objc open class ProgrammaticCollectionViewController: ScrollingViewController {
public var collectionView: UICollectionView?
open override func loadView() {
let view = UIView()
view.backgroundColor = .white
let collection = createCollectionView()
view.addSubview(collection)
NSLayoutConstraint.constraintPinSubview(toSuperview: collection)
collectionView = collection
scrollView = collectionView
self.view = view
}
/// A place to register cells with the collectionView
open func registerCells() {}
open override func viewDidLoad() {
super.viewDidLoad()
registerCells()
}
/// Creates the layout for the collection.
open func createCollectionViewLayout() -> UICollectionViewLayout {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
return layout
}
/// Creates the collection view.
open func createCollectionView() -> UICollectionView {
let collection = CollectionView(frame: .zero, collectionViewLayout: createCollectionViewLayout())
collection.dataSource = self
collection.delegate = self
return collection
}
deinit {
collectionView?.delegate = nil
collectionView?.dataSource = nil
}
}
extension ProgrammaticCollectionViewController: UICollectionViewDataSource {
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 0
}
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return UICollectionViewCell()
}
open func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
}
extension ProgrammaticCollectionViewController: UICollectionViewDelegate {
}

View File

@ -42,6 +42,11 @@ open class ProgrammaticTableViewController: ProgrammaticScrollViewController, UI
self.view = view
}
open override func viewDidLoad() {
super.viewDidLoad()
registerWithTable()
}
/// This class should create the table view that will be used here. Subclass this for different table styles.
open func createTableView() -> UITableView {
let tableView = UITableView(frame: .zero, style: .grouped)

View File

@ -0,0 +1,223 @@
//
// ThreeLayerCollectionViewController.swift
// MVMCoreUI
//
// Created by Scott Pfeil on 4/6/20.
// Copyright © 2020 Verizon Wireless. All rights reserved.
//
import Foundation
/// A view controller that has three main layers, a header, collection rows, and a footer. The header is added as a supplement header to the first section, and the footer is added as a supplement footer to the last section. This view controller allows for flexible space between the three layers to fit the screeen.
@objc open class ThreeLayerCollectionViewController: ProgrammaticCollectionViewController, UICollectionViewDelegateFlowLayout {
private var topView: UIView?
private var bottomView: UIView?
private var headerView: ContainerCollectionReusableView?
private var footerView: ContainerCollectionReusableView?
private let headerID = "header"
private let footerID = "footer"
/// Updates the padding for flexible space (header or footer)
private func updateFlexibleSpace() {
guard let tableView = collectionView else { return }
let minimumSpace: CGFloat = minimumFillSpace()
var currentSpace: CGFloat = 0
var totalMinimumSpace: CGFloat = 0
var fillTop = false
if spaceBelowTopView() == nil, headerView != nil {
fillTop = true
currentSpace += headerView?.bottomConstraint?.constant ?? 0
totalMinimumSpace += minimumSpace
}
var fillBottom = false
if spaceAboveBottomView() == nil, footerView != nil {
fillBottom = true
currentSpace += footerView?.topConstraint?.constant ?? 0
totalMinimumSpace += minimumSpace
}
guard fillTop || fillBottom else { return }
let newSpace = MVMCoreUIUtility.getVariableConstraintHeight(currentSpace, in: tableView, minimumHeight: totalMinimumSpace)
// If the bottom view is outside of the scroll, then only the top view constraint is being used, so we have to double it to account for the bottom constraint not being there when we compare to the new value.
var currentSpaceForCompare: CGFloat = currentSpace
if fillTop {
currentSpaceForCompare = currentSpace * 2;
}
if !MVMCoreGetterUtility.cgfequalwiththreshold(newSpace, currentSpaceForCompare, 1) {
if fillTop && fillBottom {
// space both
let half = newSpace / 2
headerView?.bottomConstraint?.constant = half
footerView?.topConstraint?.constant = half
collectionView?.collectionViewLayout.invalidateLayout()
} else if fillTop {
// Only top is spaced (half the size if the bottom view is out of the scroll because it needs to be sized as if there are two spacers but there is only one.
headerView?.bottomConstraint?.constant = newSpace
collectionView?.collectionViewLayout.invalidateLayout()
} else if fillBottom {
// Only bottom is spaced.
footerView?.topConstraint?.constant = newSpace
collectionView?.collectionViewLayout.invalidateLayout()
}
}
}
//MARK: - ViewController
open override func updateViews() {
super.updateViews()
// Needed due to dispatch in reloadCollectionData.
DispatchQueue.main.async {
let width = self.view.bounds.width
if let topView = self.topView as? MVMCoreViewProtocol {
topView.updateView(width)
}
if let bottomView = self.bottomView as? MVMCoreViewProtocol {
bottomView.updateView(width)
}
if let cells = self.collectionView?.visibleCells {
for cell in cells {
self.update(cell: cell, size: width)
}
}
self.invalidateCollectionLayout()
}
}
open override func handleNewData() {
super.handleNewData()
createViewForHeader()
createViewForFooter()
reloadCollectionData()
}
override open func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
//MARK: - Spacing
// If both are subclassed to return a value, then the buttons will not be pinned towards the bottom because neither spacing would try to fill the screen.
/// Space between the top view and the collection rows, nil to fill. 0 default
open func spaceBelowTopView() -> CGFloat? {
return 0
}
/// Space between the bottom view and the collection rows, nil to fill. nil default
open func spaceAboveBottomView() -> CGFloat? {
return nil
}
/// can override to return a minimum fill space. 0 default
open func minimumFillSpace() -> CGFloat {
return 0
}
//MARK: - Header Footer
/// Creates the top view.
open func createViewForHeader() {
guard let topView = viewForTop() else {
self.topView = nil
self.headerView = nil
return
}
self.topView = topView
}
/// Creates the footer
open func createViewForFooter() {
guard let bottomView = viewForBottom() else {
self.bottomView = nil
self.footerView = nil
return
}
self.bottomView = bottomView
}
//MARK: - Functions to subclass
/// Subclass for a top view.
open func viewForTop() -> UIView? {
return nil
}
/// Subclass for a bottom view.
open func viewForBottom() -> UIView? {
return nil
}
//MARK: - Collection
/// Should be used to refresh the layout of the collection view. Updates flexible padding.
open func invalidateCollectionLayout() {
self.collectionView?.collectionViewLayout.invalidateLayout()
// TODO: Improve this workaround (autolayout for cells happens async so contentSize isn't accurate immediately)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
self.updateFlexibleSpace()
})
}
/// Should be used to reload the data of the collection view. Updates flexible padding.
open func reloadCollectionData() {
collectionView?.reloadData()
// TODO: Improve this workaround (autolayout for cells happens async so contentSize isn't accurate immediately)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
self.updateFlexibleSpace()
})
}
/// Called in updateView, updates the cell.
open func update(cell: UICollectionViewCell, size: CGFloat) {
(cell as? MVMCoreViewProtocol)?.updateView(size)
}
open override func registerCells() {
super.registerCells()
collectionView?.register(ContainerCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: headerID)
collectionView?.register(ContainerCollectionReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: footerID)
}
open override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 0
}
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
guard let _ = topView,
section == 0 else { return .zero }
// Calculate the height of the header since apple doesn't support autolayout. Width is fixed, height is tall as content.
let header = headerView ?? self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: IndexPath(row: 0, section: section))
return header.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
guard let _ = bottomView,
section == numberOfSections(in: collectionView) - 1 else { return .zero }
// Calculate the height of the footr since apple doesn't support autolayout. Width is fixed, height is tall as content.
let footer = footerView ?? self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: IndexPath(row: 0, section: section))
return footer.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height), withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
}
open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
if kind == UICollectionView.elementKindSectionFooter,
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: footerID, for: indexPath) as? ContainerCollectionReusableView {
footerView.addAndContain(view: bottomView!)
footerView.topConstraint?.constant = spaceAboveBottomView() ?? 0
self.footerView = footerView
return footerView
} else if kind == UICollectionView.elementKindSectionHeader,
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: headerID, for: indexPath) as? ContainerCollectionReusableView {
headerView.addAndContain(view: topView!)
headerView.bottomConstraint?.constant = spaceBelowTopView() ?? 0
self.headerView = headerView
return headerView
}
fatalError()
}
}

View File

@ -33,7 +33,7 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController {
bottomView.updateView(width)
showFooter(width)
}
self.tableView?.reloadData()
tableView?.reloadData()
}
open override func handleNewData() {

View File

@ -24,7 +24,7 @@ import UIKit
public var formValidator: FormValidator?
public var needsUpdateUI = true
public var needsUpdateUI = false
private var observingForResponses = false
private var initialLoadFinished = false
private var previousScreenSize = CGSize.zero
@ -151,8 +151,8 @@ import UIKit
/// Calls processNewData and then sets the ui to update with updateView
open func handleNewDataAndUpdateUI() {
handleNewData()
self.needsUpdateUI = true
self.view.setNeedsLayout()
needsUpdateUI = true
view.setNeedsLayout()
}
/// Processes any new data. Called after the page is loaded the first time and on response updates for this page,
@ -166,9 +166,9 @@ import UIKit
navigationModel.line = LineModel(type: .none)
}
pageModel?.navigationItem = navigationModel
if self.formValidator == nil {
if formValidator == nil {
let rules = pageModel?.formRules
self.formValidator = FormValidator(rules)
formValidator = FormValidator(rules)
}
}
@ -265,10 +265,10 @@ import UIKit
initialLoad()
}
// Handle data on load
handleNewData()
view.setNeedsLayout()
// Handle data for first load. Dispatched to allow subclasses to finish their view did load implementations.
DispatchQueue.main.async {
self.handleNewDataAndUpdateUI()
}
}
open override func viewDidLayoutSubviews() {
@ -278,7 +278,8 @@ import UIKit
return
}
if needsUpdateUI || screenSizeChanged() {
// First update should be explicit (hence the zero check)
if needsUpdateUI || (previousScreenSize != .zero && screenSizeChanged()) {
updateViews()
needsUpdateUI = false
}

View File

@ -74,12 +74,13 @@ import MVMCore
/// Validates all rule groups. Returns if valid
public func validate() -> Bool {
var valid = true
guard let formRules = formRules else {
return valid
}
guard let formRules = formRules else { return valid }
for group in formRules {
valid = valid && validateGroup(group)
let groupValid = validateGroup(group)
valid = valid && groupValid
}
return valid
}
@ -137,13 +138,16 @@ import MVMCore
// TODO: Temporary hacks, rewrite architecture to support this.
public extension FormValidator {
func getGroupName(forPageType pageType: String?) -> String? {
for actionItem in groupWatchers {
if let buttonModel = actionItem as? ButtonModel,
pageType == (buttonModel.action as? ActionOpenPageModel)?.pageType {
return buttonModel.groupName
}
}
return nil
}
}

View File

@ -11,19 +11,35 @@ import Foundation
open class FormGroupRule: Codable {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
var groupName: String
var rules: [RulesProtocol]
//--------------------------------------------------
// MARK: - Initializer
//--------------------------------------------------
init(_ groupName: String, _ rules: [RulesProtocol]) {
self.groupName = groupName
self.rules = rules
}
//--------------------------------------------------
// MARK: - Keys
//--------------------------------------------------
private enum CodingKeys: String, CodingKey {
case groupName
case rules
}
//--------------------------------------------------
// MARK: - Codec
//--------------------------------------------------
required public init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)
self.groupName = try typeContainer.decode(String.self, forKey: .groupName)

View File

@ -9,11 +9,18 @@
import Foundation
public class RuleAllValueChangedModel: RulesProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "allValueChanged"
public var type: String = RuleAllValueChangedModel.identifier
public var fields: [String]
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
public func isValid(_ formField: FormFieldProtocol) -> Bool {
return formField.baseValue != formField.formFieldValue()
}

View File

@ -10,31 +10,41 @@ import Foundation
public class RuleEqualsModel: RulesProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "equals"
public var type: String = RuleEqualsModel.identifier
public var fields: [String]
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
public func isValid(_ formField: FormFieldProtocol) -> Bool {
return false
}
public func isValid(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool {
var valid = true
var compareValue: AnyHashable?
for formKey in fields {
guard let formField = fieldMolecules[formKey] else {
continue
}
guard let formField = fieldMolecules[formKey] else { continue }
if compareValue == nil {
compareValue = formField.formFieldValue()
continue
}
if compareValue != formField.formFieldValue(){
if compareValue != formField.formFieldValue() {
valid = false
break
}
}
return valid
}
}

View File

@ -8,16 +8,27 @@
import Foundation
public class RuleRegexModel: RulesProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "regex"
public var type: String = RuleRegexModel.identifier
public var fields: [String]
public var regex: String
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public func isValid(_ formField: FormFieldProtocol) -> Bool {
if let stringToValidate = formField.formFieldValue() as? String {
return MVMCoreUIUtility.validate(stringToValidate, withRegularExpression: regex)
}
return false
}
}

View File

@ -10,21 +10,30 @@ import Foundation
public class RuleRequiredModel: RulesProtocol {
//--------------------------------------------------
// MARK: - Properties
//--------------------------------------------------
public static var identifier: String = "allRequired"
public var type: String = RuleRequiredModel.identifier
public var fields: [String]
//--------------------------------------------------
// MARK: - Validation
//--------------------------------------------------
public func isValid(_ formField: FormFieldProtocol) -> Bool {
guard let value = formField.formFieldValue() else {
return false
}
guard let value = formField.formFieldValue() else { return false }
var valid = true
if let valueString = value as? String {
valid = valueString.count > 0
} else if let valueBool = value as? Bool {
valid = valueBool
}
return valid
}
}

View File

@ -24,7 +24,8 @@
@"list" : [[MVMCoreViewControllerProgrammaticMappingObject alloc] initWithClass:[MoleculeListTemplate class]],
@"threeLayer" : [[MVMCoreViewControllerProgrammaticMappingObject alloc] initWithClass:[ThreeLayerTemplate class]],
@"modalStack" : [[MVMCoreViewControllerProgrammaticMappingObject alloc] initWithClass:[ModalMoleculeStackTemplate class]],
@"modalList" : [[MVMCoreViewControllerProgrammaticMappingObject alloc] initWithClass:[ModalMoleculeListTemplate class]]
@"modalList" : [[MVMCoreViewControllerProgrammaticMappingObject alloc] initWithClass:[ModalMoleculeListTemplate class]],
@"collection" : [[MVMCoreViewControllerProgrammaticMappingObject alloc] initWithClass:[CollectionTemplate class]]
} mutableCopy];
});
return viewControllerMapping;

View File

@ -164,11 +164,10 @@
CGFloat topInset = scrollview.contentInset.top;
CGFloat bottomInset = scrollview.contentInset.bottom;
if (scrollview.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAutomatic) {
if (scrollview.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAutomatic || scrollview.contentInsetAdjustmentBehavior == UIScrollViewContentInsetAdjustmentAlways) {
topInset = scrollview.adjustedContentInset.top;
bottomInset = scrollview.adjustedContentInset.bottom;
}
CGFloat remainingSpace = frameHeight - contentSizeHeight - topInset - bottomInset;
return remainingSpace - 1;
}