diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 3fce1455..9ba02cb8 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ 01EB369323609801006832FA /* HeaderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EB368C23609801006832FA /* HeaderModel.swift */; }; 01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EB368D23609801006832FA /* HeadlineBodyModel.swift */; }; 01F2A03223A4498200D954D8 /* CaretLinkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F2A03123A4498200D954D8 /* CaretLinkModel.swift */; }; + 0A0FEC7425D42A5E00AF2548 /* BaseItemPickerEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0FEC7325D42A5E00AF2548 /* BaseItemPickerEntryField.swift */; }; + 0A0FEC7825D42A8500AF2548 /* BaseItemPickerEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0FEC7725D42A8500AF2548 /* BaseItemPickerEntryFieldModel.swift */; }; 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */; }; 0A1B4A96233BB18F005B3FB4 /* CheckboxLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7BAFA2232BE63400FB8E22 /* CheckboxLabel.swift */; }; 0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A21DB7E235DECC500C160A2 /* EntryField.swift */; }; @@ -94,7 +96,7 @@ 0A7EF85D23D8A95600B2AAD1 /* TextEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */; }; 0A7EF85F23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */; }; 0A7EF86123D8AC2500B2AAD1 /* DigitEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86023D8AC2500B2AAD1 /* DigitEntryFieldModel.swift */; }; - 0A7EF86323D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift */; }; + 0A7EF86323D8AFA000B2AAD1 /* BaseDropdownFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownFieldModel.swift */; }; 0A7EF86523D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */; }; 0A7EF86723D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */; }; 0A849EFE246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A849EFD246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift */; }; @@ -105,6 +107,7 @@ 0A9D09212433796500D2E6C0 /* CarouselIndicatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091B2433796500D2E6C0 /* CarouselIndicatorModel.swift */; }; 0A9D09222433796500D2E6C0 /* CarouselIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */; }; 0AA33B3A2398524F0067DD0F /* Toggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA33B392398524F0067DD0F /* Toggle.swift */; }; + 0AA4D2E125CAEC72008DB32D /* AccessibilityModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AA4D2E025CAEC72008DB32D /* AccessibilityModelProtocol.swift */; }; 0AB000BA24BF63490090C5E7 /* ModalListPageTemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB000B924BF63490090C5E7 /* ModalListPageTemplateModel.swift */; }; 0AB000BC24BF64A50090C5E7 /* ModalStackPageTemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB000BB24BF64A50090C5E7 /* ModalStackPageTemplateModel.swift */; }; 0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AB764D024460F6300E7FE72 /* UIDatePicker+Extension.swift */; }; @@ -113,6 +116,8 @@ 0ABD1371237DB0450081388D /* ItemDropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ABD1370237DB0450081388D /* ItemDropdownEntryField.swift */; }; 0AD93A9F24C0AA5100E56A97 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7918F423F5E7EA00772FF4 /* ImageView.swift */; }; 0AE14F64238315D2005417F8 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE14F63238315D2005417F8 /* TextField.swift */; }; + 0AE277E925D2ED4B0048A38D /* MultiItemDropdownEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE277E825D2ED4B0048A38D /* MultiItemDropdownEntryField.swift */; }; + 0AE277EC25D2EE310048A38D /* MultiItemDropdownEntryFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE277EB25D2EE310048A38D /* MultiItemDropdownEntryFieldModel.swift */; }; 0AE98BAF23FEF956004C5109 /* ExternalLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE98BAE23FEF956004C5109 /* ExternalLink.swift */; }; 0AE98BB323FF0934004C5109 /* ExternalLinkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE98BB223FF0934004C5109 /* ExternalLinkModel.swift */; }; 0AE98BB523FF18D2004C5109 /* Arrow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE98BB423FF18D2004C5109 /* Arrow.swift */; }; @@ -290,6 +295,7 @@ BBC0C4FF24811DCA0087C44F /* TagModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBC0C4FE24811DCA0087C44F /* TagModel.swift */; }; C003506123AA94CD00B6AC29 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003506023AA94CD00B6AC29 /* Button.swift */; }; C07065C42395677300FBF997 /* Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07065C32395677300FBF997 /* Link.swift */; }; + C6687441259D92D400F32D13 /* ActionTopNotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6687440259D92D400F32D13 /* ActionTopNotificationModel.swift */; }; C695A67F23C9830600BFB94E /* UnOrderedListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C695A67E23C9830600BFB94E /* UnOrderedListModel.swift */; }; C695A68123C9830D00BFB94E /* NumberedListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C695A68023C9830D00BFB94E /* NumberedListModel.swift */; }; C695A69423C9909000BFB94E /* DoughnutChartModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C695A69323C9909000BFB94E /* DoughnutChartModel.swift */; }; @@ -345,6 +351,8 @@ D23EA800247EBD6C00D60C34 /* LabelBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23EA7FF247EBD6C00D60C34 /* LabelBarButtonItem.swift */; }; D23EA802247EBED400D60C34 /* ImageBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23EA801247EBED400D60C34 /* ImageBarButtonItem.swift */; }; D243859923A16B1800332775 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = D243859823A16B1800332775 /* Container.swift */; }; + D24918F625D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */; }; + D24918FA25D5ADBB00CAB4B1 /* PageScrolledClosureBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */; }; D2509ED12472ED9B001BFB9D /* NavigationItemModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */; }; D2509ED62472EE2F001BFB9D /* NavigationImageButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2509ED52472EE2F001BFB9D /* NavigationImageButtonModel.swift */; }; D253BB8A24574CC5002DE544 /* StackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D260106423D0CEA700764D80 /* StackModel.swift */; }; @@ -401,6 +409,11 @@ D28BA74D248589C800B75CB8 /* TabPageModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */; }; D296E14722A5984C0051EBE7 /* MVMCoreUIViewConstrainingProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; D29B771022C281F400D6ACE0 /* ModuleMolecule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */; }; + D29C558A25C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */; }; + D29C558D25C05C990082E7D6 /* BGVideoImageMolecule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */; }; + D29C559025C095210082E7D6 /* Video.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C558F25C095210082E7D6 /* Video.swift */; }; + D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C559225C0992D0082E7D6 /* VideoModel.swift */; }; + D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C559525C099630082E7D6 /* VideoDataManager.swift */; }; D29C94D5242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */; }; D29DF0D121E404D4003B2FB9 /* MVMCoreUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; D29DF0E621E4F3C7003B2FB9 /* MVMCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D29DF0E521E4F3C7003B2FB9 /* MVMCore.framework */; }; @@ -603,6 +616,8 @@ 01EB368C23609801006832FA /* HeaderModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderModel.swift; sourceTree = ""; }; 01EB368D23609801006832FA /* HeadlineBodyModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeadlineBodyModel.swift; sourceTree = ""; }; 01F2A03123A4498200D954D8 /* CaretLinkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaretLinkModel.swift; sourceTree = ""; }; + 0A0FEC7325D42A5E00AF2548 /* BaseItemPickerEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemPickerEntryField.swift; sourceTree = ""; }; + 0A0FEC7725D42A8500AF2548 /* BaseItemPickerEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseItemPickerEntryFieldModel.swift; sourceTree = ""; }; 0A12149F22C11A17007C7030 /* ActionDetailWithImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionDetailWithImage.swift; sourceTree = ""; }; 0A209CD223A7E2810068F8B0 /* UIStackViewAlignment+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackViewAlignment+Extension.swift"; sourceTree = ""; }; 0A21DB7E235DECC500C160A2 /* EntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntryField.swift; sourceTree = ""; }; @@ -636,7 +651,7 @@ 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextEntryFieldModel.swift; sourceTree = ""; }; 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MdnEntryFieldModel.swift; sourceTree = ""; }; 0A7EF86023D8AC2500B2AAD1 /* DigitEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitEntryFieldModel.swift; sourceTree = ""; }; - 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownEntryFieldModel.swift; sourceTree = ""; }; + 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseDropdownFieldModel.swift; sourceTree = ""; }; 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDropdownEntryFieldModel.swift; sourceTree = ""; }; 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateDropdownEntryFieldModel.swift; sourceTree = ""; }; 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DigitBox.swift; sourceTree = ""; }; @@ -649,6 +664,7 @@ 0A9D091C2433796500D2E6C0 /* CarouselIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarouselIndicator.swift; sourceTree = ""; }; 0AA33B33239813C50067DD0F /* UIColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extension.swift"; sourceTree = ""; }; 0AA33B392398524F0067DD0F /* Toggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toggle.swift; sourceTree = ""; }; + 0AA4D2E025CAEC72008DB32D /* AccessibilityModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityModelProtocol.swift; sourceTree = ""; }; 0AB000B924BF63490090C5E7 /* ModalListPageTemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalListPageTemplateModel.swift; sourceTree = ""; }; 0AB000BB24BF64A50090C5E7 /* ModalStackPageTemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalStackPageTemplateModel.swift; sourceTree = ""; }; 0AB764D024460F6300E7FE72 /* UIDatePicker+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDatePicker+Extension.swift"; sourceTree = ""; }; @@ -656,6 +672,8 @@ 0ABD136C237CAD1E0081388D /* DateDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateDropdownEntryField.swift; sourceTree = ""; }; 0ABD1370237DB0450081388D /* ItemDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemDropdownEntryField.swift; sourceTree = ""; }; 0AE14F63238315D2005417F8 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; + 0AE277E825D2ED4B0048A38D /* MultiItemDropdownEntryField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiItemDropdownEntryField.swift; sourceTree = ""; }; + 0AE277EB25D2EE310048A38D /* MultiItemDropdownEntryFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiItemDropdownEntryFieldModel.swift; sourceTree = ""; }; 0AE98BAE23FEF956004C5109 /* ExternalLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLink.swift; sourceTree = ""; }; 0AE98BB223FF0934004C5109 /* ExternalLinkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLinkModel.swift; sourceTree = ""; }; 0AE98BB423FF18D2004C5109 /* Arrow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Arrow.swift; sourceTree = ""; }; @@ -833,6 +851,7 @@ BBC0C4FE24811DCA0087C44F /* TagModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagModel.swift; sourceTree = ""; }; C003506023AA94CD00B6AC29 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; C07065C32395677300FBF997 /* Link.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Link.swift; sourceTree = ""; }; + C6687440259D92D400F32D13 /* ActionTopNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionTopNotificationModel.swift; sourceTree = ""; }; C695A67E23C9830600BFB94E /* UnOrderedListModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnOrderedListModel.swift; sourceTree = ""; }; C695A68023C9830D00BFB94E /* NumberedListModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberedListModel.swift; sourceTree = ""; }; C695A69323C9909000BFB94E /* DoughnutChartModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoughnutChartModel.swift; sourceTree = ""; }; @@ -888,6 +907,8 @@ D23EA7FF247EBD6C00D60C34 /* LabelBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelBarButtonItem.swift; sourceTree = ""; }; D23EA801247EBED400D60C34 /* ImageBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageBarButtonItem.swift; sourceTree = ""; }; D243859823A16B1800332775 /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageVisibilityClosureBehavior.swift; sourceTree = ""; }; + D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageScrolledClosureBehavior.swift; sourceTree = ""; }; D2509ED02472ED9B001BFB9D /* NavigationItemModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModelProtocol.swift; sourceTree = ""; }; D2509ED52472EE2F001BFB9D /* NavigationImageButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationImageButtonModel.swift; sourceTree = ""; }; D253BB9B245874F8002DE544 /* BGImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGImageMolecule.swift; sourceTree = ""; }; @@ -942,6 +963,11 @@ D28BA74C248589C800B75CB8 /* TabPageModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPageModelProtocol.swift; sourceTree = ""; }; D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIViewConstrainingProtocol.h; sourceTree = ""; }; D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModuleMolecule.swift; sourceTree = ""; }; + D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMoleculeModel.swift; sourceTree = ""; }; + D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGVideoImageMolecule.swift; sourceTree = ""; }; + D29C558F25C095210082E7D6 /* Video.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Video.swift; sourceTree = ""; }; + D29C559225C0992D0082E7D6 /* VideoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoModel.swift; sourceTree = ""; }; + D29C559525C099630082E7D6 /* VideoDataManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDataManager.swift; sourceTree = ""; }; D29C94D4242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUICommonViewsUtility+Extension.swift"; sourceTree = ""; }; D29DF0CC21E404D4003B2FB9 /* MVMCoreUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MVMCoreUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUI.h; sourceTree = ""; }; @@ -1107,6 +1133,7 @@ 011B58EE23A2AA850085F53C /* ModelProtocols */ = { isa = PBXGroup; children = ( + 0AA4D2E025CAEC72008DB32D /* AccessibilityModelProtocol.swift */, D2E2A9A023E095AB000B42E6 /* ButtonModelProtocol.swift */, 014AA72323C501E2006F3E93 /* ContainerModelProtocol.swift */, D23EA7FA2475F09800D60C34 /* CarouselItemProtocol.swift */, @@ -1164,6 +1191,39 @@ path = FormUIHelpers; sourceTree = ""; }; + 0A0FEC7125D4246000AF2548 /* Dropdown Fields */ = { + isa = PBXGroup; + children = ( + 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownFieldModel.swift */, + 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */, + 0A0FEC8D25D4487F00AF2548 /* Date Dropdown */, + 0A0FEC8A25D4486F00AF2548 /* Item Dropdown */, + ); + path = "Dropdown Fields"; + sourceTree = ""; + }; + 0A0FEC8A25D4486F00AF2548 /* Item Dropdown */ = { + isa = PBXGroup; + children = ( + 0A0FEC7725D42A8500AF2548 /* BaseItemPickerEntryFieldModel.swift */, + 0A0FEC7325D42A5E00AF2548 /* BaseItemPickerEntryField.swift */, + 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */, + 0ABD1370237DB0450081388D /* ItemDropdownEntryField.swift */, + 0AE277EB25D2EE310048A38D /* MultiItemDropdownEntryFieldModel.swift */, + 0AE277E825D2ED4B0048A38D /* MultiItemDropdownEntryField.swift */, + ); + path = "Item Dropdown"; + sourceTree = ""; + }; + 0A0FEC8D25D4487F00AF2548 /* Date Dropdown */ = { + isa = PBXGroup; + children = ( + 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */, + 0ABD136C237CAD1E0081388D /* DateDropdownEntryField.swift */, + ); + path = "Date Dropdown"; + sourceTree = ""; + }; 0A5D59C323AD488600EFD9E9 /* Protocols */ = { isa = PBXGroup; children = ( @@ -1223,6 +1283,8 @@ children = ( 27F973522466074500CAB5C5 /* PageBehavior.swift */, 27F97369246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift */, + D24918F525D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift */, + D24918F925D5ADBA00CAB4B1 /* PageScrolledClosureBehavior.swift */, ); path = Behaviors; sourceTree = ""; @@ -1308,6 +1370,7 @@ D2ED27E9254B0CE600A1C293 /* ActionAlertModel.swift */, D2ED27EA254B0CE700A1C293 /* AlertModel.swift */, D2ED27E8254B0CE600A1C293 /* ActionPopupModel.swift */, + C6687440259D92D400F32D13 /* ActionTopNotificationModel.swift */, ); path = Actions; sourceTree = ""; @@ -1673,6 +1736,8 @@ D29B770F22C281F400D6ACE0 /* ModuleMolecule.swift */, D253BB9D2458751F002DE544 /* BGImageMoleculeModel.swift */, D253BB9B245874F8002DE544 /* BGImageMolecule.swift */, + D29C558925C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift */, + D29C558C25C05C990082E7D6 /* BGVideoImageMolecule.swift */, ); path = OtherContainers; sourceTree = ""; @@ -1707,6 +1772,8 @@ D2092348244A51D40044AD09 /* RadioSwatchModel.swift */, AAB9C109243496DD00151545 /* RadioSwatch.swift */, AA85236B244435A20059CC1E /* RadioSwatchCollectionViewCell.swift */, + D260105223CEA61600764D80 /* ToggleModel.swift */, + 0AA33B392398524F0067DD0F /* Toggle.swift */, AAA7CD68250641F90045B959 /* HeartModel.swift */, AAA7CD6A250642080045B959 /* Heart.swift */, ); @@ -2004,8 +2071,6 @@ D28A838223CCBD3F00DFE4FC /* WheelModel.swift */, 943784F3236B77BB006A1E82 /* Wheel.swift */, 943784F4236B77BB006A1E82 /* WheelAnimationHandler.swift */, - D260105223CEA61600764D80 /* ToggleModel.swift */, - 0AA33B392398524F0067DD0F /* Toggle.swift */, 0AE98BB623FF18E9004C5109 /* ArrowModel.swift */, 0AE98BB423FF18D2004C5109 /* Arrow.swift */, 94382085243238D100B43AF3 /* WebViewModel.swift */, @@ -2017,6 +2082,9 @@ AA37CBD42519072F0027344C /* Stars.swift */, AA07EA902510A442009A2AE3 /* StarModel.swift */, AA07EA922510A451009A2AE3 /* Star.swift */, + D29C559525C099630082E7D6 /* VideoDataManager.swift */, + D29C559225C0992D0082E7D6 /* VideoModel.swift */, + D29C558F25C095210082E7D6 /* Video.swift */, ); path = Views; sourceTree = ""; @@ -2024,23 +2092,18 @@ D29DF22B21E6A0FA003B2FB9 /* TextFields */ = { isa = PBXGroup; children = ( - 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, - 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, 0A7EF85A23D8A52800B2AAD1 /* EntryFieldModel.swift */, 0A21DB7E235DECC500C160A2 /* EntryField.swift */, + 0A7EF85C23D8A95600B2AAD1 /* TextEntryFieldModel.swift */, + 0A41BA7E23453A6400D4C0BC /* TextEntryField.swift */, 0A7EF85E23D8ABC500B2AAD1 /* MdnEntryFieldModel.swift */, 0A21DB82235DFBC500C160A2 /* MdnEntryField.swift */, 0A8321AE2355FE9500CB7F00 /* DigitBox.swift */, 0A7EF86023D8AC2500B2AAD1 /* DigitEntryFieldModel.swift */, 0A21DB93235E24ED00C160A2 /* DigitEntryField.swift */, - 0A7EF86223D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift */, - 0A6BF4712360C56C0028F841 /* BaseDropdownEntryField.swift */, - 0A7EF86623D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift */, - 0ABD136C237CAD1E0081388D /* DateDropdownEntryField.swift */, - 0A7EF86423D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift */, - 0ABD1370237DB0450081388D /* ItemDropdownEntryField.swift */, 0A25209724645B76000FA9F6 /* TextViewEntryFieldModel.swift */, 0A25209524645AFD000FA9F6 /* TextViewEntryField.swift */, + 0A0FEC7125D4246000AF2548 /* Dropdown Fields */, ); path = TextFields; sourceTree = ""; @@ -2154,6 +2217,7 @@ D2B18B7D236090D500A9AEDC /* BaseClasses */ = { isa = PBXGroup; children = ( + 0A5D59C323AD488600EFD9E9 /* Protocols */, 0A6682B3243769C700AD3CA1 /* TextView.swift */, C003506023AA94CD00B6AC29 /* Button.swift */, D2B18B7E2360913400A9AEDC /* Control.swift */, @@ -2164,7 +2228,6 @@ BB105858248DEFF60069D008 /* UICollectionViewLeftAlignedLayout.swift */, D21B7F70243BAC1600051ABF /* CollectionViewCell.swift */, D264FAA92440F97600D98315 /* CollectionView.swift */, - 0A5D59C323AD488600EFD9E9 /* Protocols */, 0A7918F423F5E7EA00772FF4 /* ImageView.swift */, D272F5F82473163100BD1A8F /* BarButtonItem.swift */, D2EC7BDC2527B83700F540AF /* SectionHeaderFooterView.swift */, @@ -2432,6 +2495,7 @@ 32F8804824765C8400C2ACB3 /* ListLeftVariableNumberedListAllTextAndLinks.swift in Sources */, D2CAC7CF2511052300C75681 /* CollapsableNotificationModel.swift in Sources */, DBC4391822442197001AB423 /* CaretView.swift in Sources */, + C6687441259D92D400F32D13 /* ActionTopNotificationModel.swift in Sources */, C07065C42395677300FBF997 /* Link.swift in Sources */, 0A69F611241BDEA700F7231B /* RuleAnyRequiredModel.swift in Sources */, D29B771022C281F400D6ACE0 /* ModuleMolecule.swift in Sources */, @@ -2445,6 +2509,7 @@ D264FAAA2440F97600D98315 /* CollectionView.swift in Sources */, AAC23FAD24D92A0D009208DF /* ListThreeColumnSpeedTestModel.swift in Sources */, BBC0C4FF24811DCA0087C44F /* TagModel.swift in Sources */, + 0AE277EC25D2EE310048A38D /* MultiItemDropdownEntryFieldModel.swift in Sources */, 0A7BAD74232A8DC700FB8E22 /* HeadlineBodyButton.swift in Sources */, 3265B30424BCA749000D154B /* HeadersH1NoButtonsBodyText.swift in Sources */, AAA7CD69250641F90045B959 /* HeartModel.swift in Sources */, @@ -2459,6 +2524,7 @@ D2E1FADB2260D3D200AEFD8C /* MVMCoreUIDelegateObject.swift in Sources */, 94382086243238D100B43AF3 /* WebViewModel.swift in Sources */, D28764F9245A327200CB882D /* TwoLinkView.swift in Sources */, + 0AE277E925D2ED4B0048A38D /* MultiItemDropdownEntryField.swift in Sources */, D27CD40E2322EEAF00C1DC07 /* TabsTableViewCell.swift in Sources */, 0A6682B5243769C700AD3CA1 /* TextView.swift in Sources */, D224799B231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift in Sources */, @@ -2559,6 +2625,7 @@ D2CAC7CB251104E100C75681 /* NotificationXButtonModel.swift in Sources */, 014AA73123C5059B006F3E93 /* ListPageTemplateModel.swift in Sources */, AAC23FAF24D92A1E009208DF /* ListThreeColumnSpeedTest.swift in Sources */, + 0A0FEC7425D42A5E00AF2548 /* BaseItemPickerEntryField.swift in Sources */, D29DF2A221E7AF4E003B2FB9 /* MVMCoreUIUtility.m in Sources */, D29DF12B21E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m in Sources */, D2ED27ED254B0CE700A1C293 /* ActionPopupModel.swift in Sources */, @@ -2674,6 +2741,7 @@ BB55B51D244482C1002001AD /* ListRightVariablePriceChangeBodyText.swift in Sources */, 017BEB382360C6AC0024EF95 /* RadioButtonLabel.swift in Sources */, 323AC96C24C837FF00F8E4C4 /* ListThreeColumnBillChanges.swift in Sources */, + 0A0FEC7825D42A8500AF2548 /* BaseItemPickerEntryFieldModel.swift in Sources */, D28A837923C7D5BC00DFE4FC /* PageModelProtocol.swift in Sources */, D2351C7C24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift in Sources */, 017BEB7B236763000024EF95 /* LineModel.swift in Sources */, @@ -2681,6 +2749,7 @@ 94C2D9A523872C350006CF46 /* LabelAttributeFontModel.swift in Sources */, 011D958724042492000E3791 /* FormFieldProtocol.swift in Sources */, 011D95AF2407266E000E3791 /* RadioButtonModel.swift in Sources */, + D24918F625D5AD8E00CAB4B1 /* PageVisibilityClosureBehavior.swift in Sources */, D20492A624329CE200A5EED6 /* LoadImageView.swift in Sources */, 017BEB7F23676E870024EF95 /* MoleculeObjectMapping.swift in Sources */, D274CA332236A78900B01B62 /* FooterView.swift in Sources */, @@ -2738,6 +2807,7 @@ AA104AC924472DC7004D2810 /* HeadersH1ButtonModel.swift in Sources */, 0ABD1371237DB0450081388D /* ItemDropdownEntryField.swift in Sources */, D20C7009250BF99B0095B21C /* TopNotificationModel.swift in Sources */, + D29C558A25C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift in Sources */, 8D24041123E7FB9E009E23BE /* ListLeftVariableIconWithRightCaret.swift in Sources */, BB2FB3BD247E7EF200DF73CD /* Tags.swift in Sources */, AA104ADC244734EA004D2810 /* HeadersH1LandingPageHeaderModel.swift in Sources */, @@ -2765,6 +2835,7 @@ 012A88DB238ED45900FE3DA1 /* CarouselModel.swift in Sources */, D2092355244FA0FD0044AD09 /* ThreeLayerTemplateModelProtocol.swift in Sources */, 0AE14F64238315D2005417F8 /* TextField.swift in Sources */, + D24918FA25D5ADBB00CAB4B1 /* PageScrolledClosureBehavior.swift in Sources */, 0A51F3E22475CB73002E08B6 /* LoadingSpinnerModel.swift in Sources */, D2169303251E53D9002A6324 /* SectionListTemplateModel.swift in Sources */, 0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */, @@ -2777,6 +2848,7 @@ D27CD4102339057800C1DC07 /* EyebrowHeadlineBodyLink.swift in Sources */, AAB7EDF1246ADA2A00E54929 /* ListProgressBarThin.swift in Sources */, 8D070BB2241B56AD0099AC56 /* ListRightVariableTotalData.swift in Sources */, + D29C559325C0992D0082E7D6 /* VideoModel.swift in Sources */, D264FAA5243F66A500D98315 /* CollectionTemplateItemProtocol.swift in Sources */, D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */, AA71AD3E24A32FCE00ACA76F /* HeadersH2LinkModel.swift in Sources */, @@ -2840,8 +2912,10 @@ 012A889C23889E8400FE3DA1 /* TemplateModelProtocol.swift in Sources */, 52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */, D2ED2812254B0EB800A1C293 /* MVMCoreTopAlertObject.m in Sources */, + 0AA4D2E125CAEC72008DB32D /* AccessibilityModelProtocol.swift in Sources */, C003506123AA94CD00B6AC29 /* Button.swift in Sources */, DBC4391B224421A0001AB423 /* CaretLink.swift in Sources */, + D29C559025C095210082E7D6 /* Video.swift in Sources */, D264FA90243BCE6800D98315 /* ThreeLayerCollectionViewController.swift in Sources */, AA104B1C24474A76004D2810 /* HeadersH2ButtonsModel.swift in Sources */, 0A6BF4722360C56C0028F841 /* BaseDropdownEntryField.swift in Sources */, @@ -2855,7 +2929,7 @@ D2D2FCF0252B72AF0033EAAA /* MoleculeSectionFooterModel.swift in Sources */, BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */, D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */, - 0A7EF86323D8AFA000B2AAD1 /* BaseDropdownEntryFieldModel.swift in Sources */, + 0A7EF86323D8AFA000B2AAD1 /* BaseDropdownFieldModel.swift in Sources */, 0A1214A022C11A18007C7030 /* ActionDetailWithImage.swift in Sources */, D236E5B5241FEB1000C38625 /* ListTwoColumnPriceDescriptionModel.swift in Sources */, D2B18B922361E65A00A9AEDC /* CoreUIObject.swift in Sources */, @@ -2867,6 +2941,7 @@ BB6C6AC924225290005F7224 /* ListOneColumnTextWithWhitespaceDividerShortModel.swift in Sources */, C695A69423C9909000BFB94E /* DoughnutChartModel.swift in Sources */, D2D3957D252FDBCD00047B11 /* ModalSectionListTemplateModel.swift in Sources */, + D29C559625C099630082E7D6 /* VideoDataManager.swift in Sources */, 8D4687E2242E2DE400802879 /* ListFourColumnDataUsageListItemModel.swift in Sources */, D29E28DD23D7404C00ACEA85 /* ContainerHelper.swift in Sources */, 012A88C2238D7BCA00FE3DA1 /* CarouselItemModel.swift in Sources */, @@ -2884,6 +2959,7 @@ AA633B3324989ED500731E80 /* HeadersH2PricingTwoRows.swift in Sources */, 01509D8F2327EC6F00EF99AA /* MoleculeTableViewCell.swift in Sources */, 0A6682A22434DB4F00AD3CA1 /* ListLeftVariableRadioButtonBodyText.swift in Sources */, + D29C558D25C05C990082E7D6 /* BGVideoImageMolecule.swift in Sources */, EA5124FD243601600051A3A4 /* BGImageHeadlineBodyButton.swift in Sources */, 0105618D224BBE7700E1557D /* FormValidator.swift in Sources */, 01509D912327ECE600EF99AA /* CornerLabels.swift in Sources */, @@ -2957,6 +3033,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -3022,6 +3099,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; diff --git a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift new file mode 100644 index 00000000..7ca1d6aa --- /dev/null +++ b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift @@ -0,0 +1,24 @@ +// +// ActionTopNotificationModel.swift +// MVMCoreUI +// +// Created by Murugan, Vimal on 31/12/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import UIKit + +@objcMembers public class ActionTopNotificationModel: ActionModelProtocol { + + public static var identifier: String = "topNotification" + public var actionType: String = ActionTopNotificationModel.identifier + public var topNotification: TopNotificationModel + public var extraParameters: JSONValueDictionary? + public var analyticsData: JSONValueDictionary? + + public init(topNotification: TopNotificationModel, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { + self.topNotification = topNotification + self.extraParameters = extraParameters + self.analyticsData = analyticsData + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 0881af71..52ade755 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -18,6 +18,7 @@ public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupW public static var identifier: String = "button" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var title: String public var action: ActionModelProtocol public var enabled: Bool = true @@ -93,27 +94,27 @@ public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupW //-------------------------------------------------- public func enabled_fillColor() -> UIColor? { - return (inverted ? enabledFillColor_inverted : enabledFillColor)?.uiColor + (inverted ? enabledFillColor_inverted : enabledFillColor)?.uiColor } public func enabled_textColor() -> UIColor? { - return (inverted ? enabledTextColor_inverted : enabledTextColor)?.uiColor + (inverted ? enabledTextColor_inverted : enabledTextColor)?.uiColor } public func enabled_borderColor() -> UIColor? { - return (inverted ? enabledBorderColor_inverted : enabledBorderColor)?.uiColor + (inverted ? enabledBorderColor_inverted : enabledBorderColor)?.uiColor } public func disabled_fillColor() -> UIColor? { - return (inverted ? disabledFillColor_inverted : disabledFillColor)?.uiColor + (inverted ? disabledFillColor_inverted : disabledFillColor)?.uiColor } public func disabled_textColor() -> UIColor? { - return (inverted ? disabledTextColor_inverted : disabledTextColor)?.uiColor + (inverted ? disabledTextColor_inverted : disabledTextColor)?.uiColor } public func disabled_borderColor() -> UIColor? { - return (inverted ? disabledBorderColor_inverted : disabledBorderColor)?.uiColor + (inverted ? disabledBorderColor_inverted : disabledBorderColor)?.uiColor } /// Defines the default appearance for the primary style. @@ -172,6 +173,7 @@ public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupW private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case title case inverted case action @@ -195,6 +197,7 @@ public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupW let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) @@ -252,6 +255,7 @@ public class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupW try container.encode(inverted, forKey: .inverted) try container.encodeModel(action, forKey: .action) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(enabledFillColor, forKey: .fillColor) try container.encodeIfPresent(enabledTextColor, forKey: .textColor) try container.encodeIfPresent(enabledBorderColor, forKey: .borderColor) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/CaretLink.swift b/MVMCoreUI/Atomic/Atoms/Buttons/CaretLink.swift index b0822480..5ae68241 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/CaretLink.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/CaretLink.swift @@ -127,7 +127,6 @@ open class CaretLink: Button, MVMCoreUIViewConstrainingProtocol { } public func updateCaretSpacing(_ spacing: CGFloat) { - caretSpacingConstraint?.constant = spacing } @@ -151,15 +150,9 @@ open class CaretLink: Button, MVMCoreUIViewConstrainingProtocol { setTitle(model.title, for: .normal) } - public func needsToBeConstrained() -> Bool { - return true - } + public func needsToBeConstrained() -> Bool { true } - open func horizontalAlignment() -> UIStackView.Alignment { - return .leading - } + open func horizontalAlignment() -> UIStackView.Alignment { .leading } - open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return 10.5 - } + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 10.5 } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift index a37baeca..25d0a539 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift @@ -17,6 +17,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea public static var identifier: String = "caretLink" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var title: String public var action: ActionModelProtocol public var enabledColor: Color = Color(uiColor: .mvmBlack) @@ -41,6 +42,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea private enum CodingKeys: String, CodingKey { case backgroundColor + case accessibilityIdentifier case title case action case enabledColor_inverted @@ -60,6 +62,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) if let enabledColor_inverted = try typeContainer.decodeIfPresent(Color.self, forKey: .enabledColor_inverted) { @@ -94,6 +97,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea try container.encode(moleculeName, forKey: .moleculeName) try container.encode(title, forKey: .title) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModel(action, forKey: .action) try container.encode(enabled, forKey: .enabledColor) try container.encodeIfPresent(disabledColor, forKey: .disabledColor) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/Link/Link.swift b/MVMCoreUI/Atomic/Atoms/Buttons/Link/Link.swift index d484840d..04140d6e 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/Link.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/Link.swift @@ -11,18 +11,18 @@ import UIKit @objcMembers open class Link: Button { //-------------------------------------------------- - // MARK: - Lifecycle + // MARK: - Draw //-------------------------------------------------- open override func draw(_ rect: CGRect) { - guard let textRect = titleLabel?.frame else { return } - - let context = UIGraphicsGetCurrentContext() + guard let textRect = titleLabel?.frame, + let context = UIGraphicsGetCurrentContext() + else { return } // Set line to the same color as the text if let color = titleLabel?.textColor?.cgColor { - context?.setStrokeColor(color) + context.setStrokeColor(color) } // x should be according to the text, not the button @@ -31,9 +31,9 @@ import UIKit // Line is 1 point below the text let y = textRect.origin.y + textRect.size.height + 1 - context?.move(to: CGPoint(x: x, y: y)) - context?.addLine(to: CGPoint(x: x + textRect.size.width, y: y)) - context?.strokePath() + context.move(to: CGPoint(x: x, y: y)) + context.addLine(to: CGPoint(x: x + textRect.size.width, y: y)) + context.strokePath() } open override var intrinsicContentSize: CGSize { @@ -58,9 +58,7 @@ import UIKit set(with: model.action, delegateObject: delegateObject, additionalData: additionalData) } - open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return 31 - } + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 31 } } // MARK: - MVMCoreViewProtocol @@ -69,16 +67,12 @@ extension Link { open override func updateView(_ size: CGFloat) { super.updateView(size) - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - var width = size - if MVMCoreGetterUtility.fequal(a: Float.leastNormalMagnitude, b: Float(size)) { - width = MVMCoreUIUtility.getWidth() - } - - self.titleLabel?.font = MFStyler.fontB2(forWidth: width) + var width = size + if MVMCoreGetterUtility.fequal(a: Float.leastNormalMagnitude, b: Float(size)) { + width = MVMCoreUIUtility.getWidth() } + + titleLabel?.font = MFStyler.fontB2(forWidth: width) } open override func setupView() { @@ -98,7 +92,5 @@ extension Link { // MARK: - MVMCoreUIViewConstrainingProtocol extension Link: MVMCoreUIViewConstrainingProtocol { - open func horizontalAlignment() -> UIStackView.Alignment { - return .leading - } + open func horizontalAlignment() -> UIStackView.Alignment { .leading } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift index bd1b601d..f3b48bd4 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift @@ -14,11 +14,10 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode // MARK: - Properties //-------------------------------------------------- - public class var identifier: String { - return "link" - } + public class var identifier: String { "link" } public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var title: String public var action: ActionModelProtocol public var enabled = true @@ -44,6 +43,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case title case action case enabled @@ -62,6 +62,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) @@ -95,6 +96,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode try container.encode(title, forKey: .title) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModel(action, forKey: .action) try container.encode(inverted, forKey: .inverted) try container.encode(enabled, forKey: .enabled) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift b/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift index f5a7ed44..8eefe1d7 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/PillButton.swift @@ -18,7 +18,7 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { var size = MVMCoreUIUtility.getWidth() var buttonModel: ButtonModel? { - get { return model as? ButtonModel } + get { model as? ButtonModel } } /// Need to re-style on set. @@ -27,9 +27,7 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { } open var buttonSize: Styler.Button.Size = .standard { - didSet { - buttonModel?.size = buttonSize - } + didSet { buttonModel?.size = buttonSize } } //-------------------------------------------------- @@ -47,12 +45,12 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { //-------------------------------------------------- public var enabledTitleColor: UIColor? { - get { return titleColor(for: .normal) } + get { titleColor(for: .normal) } set { setTitleColor(newValue, for: .normal) } } public var disabledTitleColor: UIColor? { - get { return titleColor(for: .disabled) } + get { titleColor(for: .disabled) } set { setTitleColor(newValue, for: .disabled) } } @@ -106,6 +104,11 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { self.disabledTitleColor = disabledTitleColor } + #if DEBUG + // Useful to detect with isHittable when performing UI testing. + isAccessibilityElement = isEnabled + #endif + if isEnabled { if let fillColor = buttonModel?.enabledColors.fill { backgroundColor = fillColor @@ -128,7 +131,7 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { } private func getInnerPadding() -> CGFloat { - return getHeight() / 2.0 + getHeight() / 2.0 } private func getHeight() -> CGFloat { @@ -181,9 +184,11 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { guard let model = model as? ButtonModel else { return } setTitle(model.title, for: .normal) + if let size = model.size { buttonSize = size } + model.updateUI = { [weak self] in MVMCoreDispatchUtility.performBlock(onMainThread: { self?.enableField(model.enabled) @@ -228,9 +233,7 @@ open class PillButton: Button, MVMCoreUIViewConstrainingProtocol { // MARK: - MVMCoreUIViewConstrainingProtocol //-------------------------------------------------- - open func horizontalAlignment() -> UIStackView.Alignment { - return .center - } + open func horizontalAlignment() -> UIStackView.Alignment { .center } public func enableField(_ enable: Bool) { isEnabled = enable diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitBox.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitBox.swift index 7dce16a4..912b0891 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitBox.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitBox.swift @@ -43,7 +43,7 @@ import UIKit //-------------------------------------------------- public override var showError: Bool { - get { return super.showError } + get { super.showError } set (error) { DispatchQueue.main.async { [weak self] in guard let self = self else { return } @@ -80,7 +80,7 @@ import UIKit open override func setupView() { super.setupView() - + addSubview(digitField) digitField.delegate = self digitField.didDeleteDelegate = self @@ -140,7 +140,7 @@ import UIKit super.updateView(size) if !MVMCoreGetterUtility.fequal(a: Float(size), b: Float(previousSize)) { - + var width: CGFloat = 0 var height: CGFloat = 0 var pointSize: CGFloat = 13 diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift index b1f202a8..c70d458e 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryField.swift @@ -75,7 +75,7 @@ import UIKit private var selectedDigitBox: DigitBox? public var digitEntryModel: DigitEntryFieldModel? { - return model as? DigitEntryFieldModel + model as? DigitEntryFieldModel } //-------------------------------------------------- @@ -83,7 +83,7 @@ import UIKit //-------------------------------------------------- public override var isEnabled: Bool { - get { return super.isEnabled } + get { super.isEnabled } set (enabled) { digitBoxes.forEach { $0.isEnabled = enabled } super.isEnabled = enabled @@ -91,7 +91,7 @@ import UIKit } public override var showError: Bool { - get { return super.showError } + get { super.showError } set (error) { digitBoxes.forEach { $0.showError = error } super.showError = error @@ -99,7 +99,7 @@ import UIKit } public override var isLocked: Bool { - get { return super.isLocked } + get { super.isLocked } set (locked) { digitBoxes.forEach { $0.isLocked = locked } super.isLocked = locked @@ -162,7 +162,7 @@ import UIKit /// If you're using a MFViewController, you must set this to it public override weak var uiTextFieldDelegate: UITextFieldDelegate? { - get { return textField.delegate } + get { textField.delegate } set { textField.delegate = self proprietorTextDelegate = newValue @@ -357,9 +357,7 @@ import UIKit super.set(with: model, delegateObject, additionalData) } - public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return 115 - } + public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 115 } } // MARK: - TextField Delegate @@ -452,11 +450,11 @@ extension DigitEntryField { @objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - return proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true + proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true } @objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { - return proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true + proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryFieldModel.swift index 4132a15e..c381637a 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DigitEntryFieldModel.swift @@ -12,12 +12,10 @@ // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { - return "digitTextField" - } + public override class var identifier: String { "digitTextField" } public var digits: Int = 4 - + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/BaseDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift similarity index 86% rename from MVMCoreUI/Atomic/Atoms/FormFields/TextFields/BaseDropdownEntryField.swift rename to MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift index 82e46306..5a7f1702 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/BaseDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift @@ -1,5 +1,5 @@ // -// BaseDropdownEntryField.swift +// BaseDropdownField.swift // MVMCoreUI // // Created by Kevin Christiano on 10/23/19. @@ -28,8 +28,9 @@ import UIKit }() public var baseDropdownEntryFieldModel: BaseDropdownEntryFieldModel? { - return model as? BaseDropdownEntryFieldModel + model as? BaseDropdownEntryFieldModel } + var additionalData: [AnyHashable: Any]? //-------------------------------------------------- @@ -54,7 +55,7 @@ import UIKit @objc required public init?(coder: NSCoder) { super.init(coder: coder) - fatalError("DropdownEntryField does not support xib.") + fatalError("\(String(describing: Self.self)) does not support xib.") } required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { @@ -86,15 +87,15 @@ import UIKit dropDownCaretView.setOptional(with: model.caretView, delegateObject, additionalData) } - public override func dismissFieldInput(_ sender: Any?) { - performDropdownAction() - super.dismissFieldInput(sender) - } + @objc public override func dismissFieldInput(_ sender: Any?) { + performDropdownAction() + super.dismissFieldInput(sender) + } func performDropdownAction() { - if let baseDropdownEntryFieldModel = baseDropdownEntryFieldModel, let actionModel = baseDropdownEntryFieldModel.action, let actionMap = actionModel.toJSON() { + if let baseDropdownEntryFieldModel = baseDropdownEntryFieldModel, let actionModel = baseDropdownEntryFieldModel.action { let additionalDataWithSource = additionalData.dictionaryAdding(key: KeySourceModel, value: baseDropdownEntryFieldModel) - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalDataWithSource, delegateObject: delegateObject) + MVMCoreActionHandler.shared()?.asyncHandleAction(with: actionModel, additionalData: additionalDataWithSource, delegateObject: delegateObject) } } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/BaseDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift similarity index 93% rename from MVMCoreUI/Atomic/Atoms/FormFields/TextFields/BaseDropdownEntryFieldModel.swift rename to MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift index 0bb17284..39701016 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/BaseDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift @@ -14,9 +14,7 @@ public var caretView: CaretViewModel? public var action: ActionModelProtocol? - public override class var identifier: String { - return "" - } + public override class var identifier: String { "" } //-------------------------------------------------- // MARK: - Keys @@ -29,7 +27,7 @@ } //-------------------------------------------------- - // MARK: - Initializers + // MARK: - Codec //-------------------------------------------------- required public init(from decoder: Decoder) throws { diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DateDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Date Dropdown/DateDropdownEntryField.swift similarity index 97% rename from MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DateDropdownEntryField.swift rename to MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Date Dropdown/DateDropdownEntryField.swift index 6ff950f6..234809d3 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DateDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Date Dropdown/DateDropdownEntryField.swift @@ -23,15 +23,15 @@ import UIKit }() public var dateFormat: String? { + get { dateDropdownModel?.dateFormat } set { guard let newValue = newValue else { return } dateDropdownModel?.dateFormat = newValue } - get { return dateDropdownModel?.dateFormat } } public var dateDropdownModel: DateDropdownEntryFieldModel? { - return model as? DateDropdownEntryFieldModel + model as? DateDropdownEntryFieldModel } //-------------------------------------------------- @@ -69,6 +69,7 @@ import UIKit datePicker = UIDatePicker.addDatePicker(to: textField) datePicker?.addTarget(self, action: #selector(pickerValueChanged), for: .valueChanged) datePicker?.timeZone = NSTimeZone.system + textField.inputView = datePicker UIToolbar.addDismissToolbar(to: textField, delegate: self, action: #selector(dismissFieldInput)) } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DateDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Date Dropdown/DateDropdownEntryFieldModel.swift similarity index 96% rename from MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DateDropdownEntryFieldModel.swift rename to MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Date Dropdown/DateDropdownEntryFieldModel.swift index 0ac448a2..53c7206c 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/DateDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Date Dropdown/DateDropdownEntryFieldModel.swift @@ -11,9 +11,7 @@ // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { - return "dateDropdownEntryField" - } + public override class var identifier: String { "dateDropdownEntryField" } public var dateFormatter: DateFormatter = { let formatter = DateFormatter() diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift new file mode 100644 index 00000000..00b5a072 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryField.swift @@ -0,0 +1,85 @@ +// +// BaseItemPickerEntryField.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/10/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import UIKit + +public typealias TextFieldAndPickerDelegate = (UITextFieldDelegate & UIPickerViewDelegate & UIPickerViewDataSource) + + +open class BaseItemPickerEntryField: BaseDropdownEntryField, UIPickerViewDelegate, UIPickerViewDataSource { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + + open lazy var pickerView = UIPickerView.addPicker(to: textField, delegate: self, dismissAction: #selector(dismissFieldInput)) + + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + /// Closure passed here will run as picker changes items. + public var observeDropdownChange: ((String, String) -> ())? + + /// Closure passed here will run upon dismissing the selection picker. + public var observeDropdownSelection: ((String) -> ())? + + /// When selecting for first responder, allow initial selected value to appear in empty text field. + public var setInitialValueInTextField = true + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + @objc open override func setupFieldContainerContent(_ container: UIView) { + super.setupFieldContainerContent(container) + + textField.hideBlinkingCaret = true + textField.autocorrectionType = .no + uiTextFieldDelegate = self + } + + @objc public func setPickerDelegates(delegate: UIPickerViewDelegate & UIPickerViewDataSource) { + + pickerView.delegate = delegate + pickerView.dataSource = delegate + } + + //-------------------------------------------------- + // MARK: - Molecular + //-------------------------------------------------- + + public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + + setPickerDelegates(delegate: self) + } + + //-------------------------------------------------- + // MARK: - Picker Delegate to Override + //-------------------------------------------------- + + public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { 0 } + + public func numberOfComponents(in pickerView: UIPickerView) -> Int { 0 } +} + + +// MARK: - Accessibility +extension BaseItemPickerEntryField { + + @objc open override func setAccessibilityString(_ accessibilityString: String?) { + + var accessibilityString = accessibilityString ?? "" + + if let textPickerItem = MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item") { + accessibilityString += textPickerItem + } + + textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" + } +} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryFieldModel.swift new file mode 100644 index 00000000..a2b3d696 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/BaseItemPickerEntryFieldModel.swift @@ -0,0 +1,18 @@ +// +// BaseItemPickerEntryFieldModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/10/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class BaseItemPickerEntryFieldModel: BaseDropdownEntryFieldModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public override class var identifier: String { "" } +} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/ItemDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift similarity index 54% rename from MVMCoreUI/Atomic/Atoms/FormFields/TextFields/ItemDropdownEntryField.swift rename to MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift index 47c59dae..b3aeb172 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/ItemDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryField.swift @@ -8,28 +8,16 @@ import UIKit -public typealias TextFieldAndPickerDelegate = (UITextFieldDelegate & UIPickerViewDelegate & UIPickerViewDataSource) - -open class ItemDropdownEntryField: BaseDropdownEntryField { +open class ItemDropdownEntryField: BaseItemPickerEntryField { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- open var pickerData: [String] = [] - open var pickerView: UIPickerView? - - /// When selecting for first responder, allow initial selected value to appear in empty text field. - public var setInitialValueInTextField = true - - /// Closure passed here will run as picker changes items. - public var observeDropdownChange: ((String, String)->())? - - /// Closure passed here will run upon dismissing the selection picker. - public var observeDropdownSelection: ((String)->())? public var itemDropdownEntryFieldModel: ItemDropdownEntryFieldModel? { - return model as? ItemDropdownEntryFieldModel + model as? ItemDropdownEntryFieldModel } //-------------------------------------------------- @@ -61,26 +49,13 @@ open class ItemDropdownEntryField: BaseDropdownEntryField { // MARK: - Methods //-------------------------------------------------- - @objc open override func setupFieldContainerContent(_ container: UIView) { - super.setupFieldContainerContent(container) - - pickerView = UIPickerView.addPicker(to: textField, delegate: self, dismissAction: #selector(dismissFieldInput)) - textField.hideBlinkingCaret = true - textField.autocorrectionType = .no - uiTextFieldDelegate = self - } - - @objc public func setPickerDelegates(delegate: UIPickerViewDelegate & UIPickerViewDataSource) { - - pickerView?.delegate = delegate - pickerView?.dataSource = delegate - } - + /// Sets the textField with the first value of the available picker data. @objc private func setInitialValueFromPicker() { guard !pickerData.isEmpty else { return } - if setInitialValueInTextField, let pickerIndex = pickerView?.selectedRow(inComponent: 0) { + if setInitialValueInTextField { + let pickerIndex = pickerView.selectedRow(inComponent: 0) observeDropdownChange?(text ?? "", pickerData[pickerIndex]) text = pickerData[pickerIndex] itemDropdownEntryFieldModel?.selectedIndex = pickerIndex @@ -98,9 +73,7 @@ open class ItemDropdownEntryField: BaseDropdownEntryField { guard !pickerData.isEmpty else { return } - if let pickerIndex = pickerView?.selectedRow(inComponent: 0) { - observeDropdownSelection?(pickerData[pickerIndex]) - } + observeDropdownSelection?(pickerData[pickerView.selectedRow(inComponent: 0)]) } public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { @@ -109,23 +82,21 @@ open class ItemDropdownEntryField: BaseDropdownEntryField { guard let model = model as? ItemDropdownEntryFieldModel else { return } pickerData = model.options - setPickerDelegates(delegate: self) - if let pickerView = pickerView, let index = model.selectedIndex { + if let index = model.selectedIndex { + self.pickerView.selectRow(index, inComponent: 0, animated: false) self.pickerView(pickerView, didSelectRow: index, inComponent: 0) } } -} -// MARK:- Base Picker Delegate -extension ItemDropdownEntryField: UIPickerViewDelegate, UIPickerViewDataSource { + //-------------------------------------------------- + // MARK: - Picker Delegate + //-------------------------------------------------- - @objc public func numberOfComponents(in pickerView: UIPickerView) -> Int { - return 1 - } + @objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 } - @objc public func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { - return pickerData.count + @objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + pickerData.count } @objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { @@ -142,18 +113,3 @@ extension ItemDropdownEntryField: UIPickerViewDelegate, UIPickerViewDataSource { itemDropdownEntryFieldModel?.selectedIndex = row } } - -// MARK: - Accessibility -extension ItemDropdownEntryField { - - @objc open override func setAccessibilityString(_ accessibilityString: String?) { - - var accessibilityString = accessibilityString ?? "" - - if let textPickerItem = MVMCoreUIUtility.hardcodedString(withKey: "textfield_picker_item") { - accessibilityString += textPickerItem - } - - textField.accessibilityLabel = "\(accessibilityString) \(textField.isEnabled ? "" : MVMCoreUIUtility.hardcodedString(withKey: "textfield_disabled_state") ?? "")" - } -} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/ItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift similarity index 78% rename from MVMCoreUI/Atomic/Atoms/FormFields/TextFields/ItemDropdownEntryFieldModel.swift rename to MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift index 0bc1ee0a..bf7416e6 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/ItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift @@ -6,22 +6,24 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -@objcMembers open class ItemDropdownEntryFieldModel: BaseDropdownEntryFieldModel { +@objcMembers open class ItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { - return "dropDown" - } + public override class var identifier: String { "dropDown" } public var options: [String] = [] public var selectedIndex: Int? - + + //-------------------------------------------------- + // MARK: - Validation + //-------------------------------------------------- + public override func formFieldValue() -> AnyHashable? { guard !options.isEmpty, - let index = selectedIndex - else { return nil } + let index = selectedIndex + else { return nil } return options[index] } @@ -34,7 +36,7 @@ case options case selectedIndex } - + //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- @@ -47,10 +49,7 @@ if let selectedIndex = try typeContainer.decodeIfPresent(Int.self, forKey: .selectedIndex) { self.selectedIndex = selectedIndex - } - - if let index = selectedIndex { - baseValue = options.indices.contains(index) ? options[index] : nil + baseValue = options.indices.contains(selectedIndex) ? options[selectedIndex] : nil } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryField.swift new file mode 100644 index 00000000..8bed3996 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryField.swift @@ -0,0 +1,144 @@ +// +// MultiItemDropdownEndryField.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/9/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import UIKit + + +open class MultiItemDropdownEntryField: BaseItemPickerEntryField { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + /// Datasource of the picker view. + open var pickerComponents: [[String]] { + dropdownModel?.components ?? [[]] + } + + public var dropdownModel: MultiItemDropdownEntryFieldModel? { + model as? MultiItemDropdownEntryFieldModel + } + + /// The number of components available + public var componentCount: Int { + pickerComponents.count + } + + //-------------------------------------------------- + // MARK: - Initializers + //-------------------------------------------------- + + @objc public override init(frame: CGRect) { + super.init(frame: frame) + } + + @objc public convenience init() { + self.init(frame: .zero) + } + + @objc required public init?(coder: NSCoder) { + fatalError("MultiItemDropdownEntryField init(coder:) has not been implemented") + } + + required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.init(model: model, delegateObject, additionalData) + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + /// Sets the textField with the first value of the available picker data. + @objc private func setInitialValueFromPicker() { + + guard setInitialValueInTextField, + !pickerComponents.isEmpty, + let rowText = dropdownModel?.selectedRowText + else { return } + + // Update observing function and update text UI. + observeDropdownChange?(text ?? "", rowText) + text = rowText + + // Set row index value of selected component. + for component in 0.. Bool { + !pickerComponents.isEmpty && !pickerComponents[index].isEmpty + } + + //-------------------------------------------------- + // MARK: - TextField Observation + //-------------------------------------------------- + + /// Observing action of the textfield for initial interaction with the picker. + @objc override func startEditing() { + super.startEditing() + + setInitialValueFromPicker() + } + + /// Observing action for when the user has ended inputting with the picker. + @objc override func endInputing() { + super.endInputing() + + guard !pickerComponents.isEmpty, + let rowText = dropdownModel?.selectedRowText + else { return } + + // update observing function with most recent selection. + observeDropdownSelection?(rowText) + } + + //-------------------------------------------------- + // 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? MultiItemDropdownEntryFieldModel else { return } + + // Select initial rows if selectedIndexes retains value. + for (component, row) in model.selectedIndexes { + self.pickerView.selectRow(row, inComponent: component, animated: false) + self.pickerView(pickerView, didSelectRow: row, inComponent: component) + } + } + + //-------------------------------------------------- + // MARK: - Picker Delegate + //-------------------------------------------------- + + @objc public override func numberOfComponents(in pickerView: UIPickerView) -> Int { componentCount } + + @objc public override func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + pickerComponents[component].count + } + + @objc public func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + + guard pickerHasComponent(component) else { return nil } + + return pickerComponents[component][row] + } + + @objc public func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + + guard pickerHasComponent(component) else { return } + + let oldText = text ?? "" + dropdownModel?.selectedIndexes[component] = row + let newText = dropdownModel?.selectedRowText + observeDropdownChange?(oldText, newText ?? "") + text = newText + } +} diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift new file mode 100644 index 00000000..8799f6aa --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/MultiItemDropdownEntryFieldModel.swift @@ -0,0 +1,126 @@ +// +// MultiItemDropdownEntryFieldModel.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/9/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + + +@objcMembers open class MultiItemDropdownEntryFieldModel: BaseItemPickerEntryFieldModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public override class var identifier: String { "multiDropdown" } + + public var components: [[String]] = [[]] + public var selectedIndexes: [Int: Int] = [:] + public var delimiters: [Int: String] = [:] + + //-------------------------------------------------- + // MARK: - Validation + //-------------------------------------------------- + + public override func formFieldValue() -> AnyHashable? { + + guard !components.isEmpty && !selectedIndexes.isEmpty else { return nil } + + return selectedRowText + } + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + /// - parameter index: The index of the delimiter. + /// - returns: The delimiter for a given index. Defaults to whitespace for valid index if no delimiters is provided. If invalid index, empty string. + public func delimiter(for index: Int) -> String { + + guard index != components.count - 1 else { return "" } + + return delimiters[index, default: " "] + } + + /// A string of the picker row concatenated by whitespace or delimiters if provided. + public var selectedRowText: String { + + var text = "" + + for i in 0.. CGFloat? { - return 115 - } + open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 115 } } // MARK: - Accessibility diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift index d64f70ae..2a2e737c 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift @@ -14,13 +14,19 @@ import Foundation // MARK: - Properties //-------------------------------------------------- - public class var identifier: String { - return "" - } + public class var identifier: String { "" } public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var title: String? public var feedback: String? + public var shouldClearText: Bool = false + public var dynamicErrorMessage: String? { + didSet { + isValid = dynamicErrorMessage?.isEmpty ?? true + updateUIDynamicError?() + } + } public var errorMessage: String? public var errorTextColor: Color? public var enabled: Bool = true @@ -41,6 +47,9 @@ import Foundation /// Temporary binding mechanism for the view to update on enable changes. public var updateUI: ActionBlock? + // TODO: Remove once updateUI is fixed with isSelected + public var updateUIDynamicError: ActionBlock? + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- @@ -48,6 +57,7 @@ import Foundation private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case title case enabled case feedback @@ -67,6 +77,9 @@ import Foundation //-------------------------------------------------- public func formFieldValue() -> AnyHashable? { + if dynamicErrorMessage != nil { + dynamicErrorMessage = nil + } return text } @@ -96,6 +109,7 @@ import Foundation required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decodeIfPresent(String.self, forKey: .title) feedback = try typeContainer.decodeIfPresent(String.self, forKey: .feedback) errorMessage = try typeContainer.decodeIfPresent(String.self, forKey: .errorMessage) @@ -117,6 +131,7 @@ import Foundation var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(feedback, forKey: .feedback) try container.encodeIfPresent(text, forKey: .text) @@ -125,9 +140,9 @@ import Foundation try container.encodeIfPresent(selected, forKey: .selected) try container.encodeIfPresent(errorTextColor, forKey: .errorTextColor) try container.encodeIfPresent(errorMessage, forKey: .errorMessage) - try container.encode(enabled, forKey: .enabled) - try container.encode(hideBorders, forKey: .hideBorders) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encodeIfPresent(groupName, forKey: .groupName) + try container.encode(enabled, forKey: .enabled) + try container.encode(hideBorders, forKey: .hideBorders) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index d80a008b..fe2c483c 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift @@ -31,7 +31,7 @@ import MVMCore /// If you're using a MFViewController, you must set this to it public override weak var uiTextFieldDelegate: UITextFieldDelegate? { - get { return textField.delegate } + get { textField.delegate } set { textField.delegate = self proprietorTextDelegate = newValue @@ -44,7 +44,7 @@ import MVMCore /// Formats the MDN when setting and removes format of MDN when reading. public var mdn: String? { - get { return MVMCoreUIUtility.removeMdnFormat(text) } + get { MVMCoreUIUtility.removeMdnFormat(text) } set { text = MVMCoreUIUtility.formatMdn(newValue) } } @@ -160,9 +160,9 @@ import MVMCore // Sometimes user add extra 1 in front of mdn in their address book if isNationalMDN, - let unformedMDN = unformattedMDN, - unformedMDN.count == 11, - unformedMDN[(unformedMDN.index(unformedMDN.startIndex, offsetBy: 0))] == "1" { + let unformedMDN = unformattedMDN, + unformedMDN.count == 11, + unformedMDN[(unformedMDN.index(unformedMDN.startIndex, offsetBy: 0))] == "1" { let startIndex = unformedMDN.index(unformedMDN.startIndex, offsetBy: 1) unformattedMDN = String(unformedMDN[startIndex...]) @@ -211,17 +211,14 @@ import MVMCore } @objc public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - - return proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true + proprietorTextDelegate?.textFieldShouldBeginEditing?(textField) ?? true } @objc public func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { - - return proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true + proprietorTextDelegate?.textFieldShouldEndEditing?(textField) ?? true } @objc public func textFieldShouldClear(_ textField: UITextField) -> Bool { - - return proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true + proprietorTextDelegate?.textFieldShouldClear?(textField) ?? true } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift index 0541de8d..53d0703d 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryFieldModel.swift @@ -11,7 +11,5 @@ // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { - return "mdnEntryField" - } + public override class var identifier: String { "mdnEntryField" } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift index 2c4aa736..6ff196c7 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryField.swift @@ -54,16 +54,14 @@ import UIKit /// Validate when user resigns editing. Default: true public var validateWhenDoneEditing: Bool = true - public var textEntryFieldModel: TextEntryFieldModel? { - return model as? TextEntryFieldModel - } + public var textEntryFieldModel: TextEntryFieldModel? { model as? TextEntryFieldModel } //-------------------------------------------------- // MARK: - Computed Properties //-------------------------------------------------- public override var isEnabled: Bool { - get { return super.isEnabled } + get { super.isEnabled } set (enabled) { super.isEnabled = enabled @@ -77,7 +75,7 @@ import UIKit } public override var showError: Bool { - get { return super.showError } + get { super.showError } set (error) { if error { @@ -96,16 +94,16 @@ import UIKit /// The text of this TextField. open override var text: String? { - get { return textField.text } + get { textField.text } set { - textField.text = newValue textEntryFieldModel?.text = newValue + textField.text = newValue } } /// Placeholder access for the TextField. public var placeholder: String? { - get { return textField.placeholder } + get { textField.placeholder } set { textField.placeholder = newValue } } @@ -133,7 +131,7 @@ import UIKit /// If you're using a ViewController, you must set this to it public weak var uiTextFieldDelegate: UITextFieldDelegate? { - get { return textField.delegate } + get { textField.delegate } set { textField.delegate = newValue } } @@ -222,9 +220,7 @@ import UIKit @discardableResult @objc override open func resignFirstResponder() -> Bool { - if validateWhenDoneEditing { - validateText() - } + if validateWhenDoneEditing { validateText() } textField.resignFirstResponder() isSelected = false return true @@ -239,6 +235,11 @@ import UIKit /// Executes on UITextField.textDidBeginEditingNotification @objc override func startEditing() { super.startEditing() + + if textEntryFieldModel?.clearTextOnTap ?? false { + text = "" + } + textField.becomeFirstResponder() } @@ -257,14 +258,33 @@ import UIKit showError = false return } - + if let isValid = textEntryFieldModel?.isValid { self.isValid = isValid } + regexTextFieldOutputIfAvailable() + shouldShowError(!isValid) } + func regexTextFieldOutputIfAvailable() { + + if let regex = textEntryFieldModel?.displayFormat, + let mask = textEntryFieldModel?.displayMask, + let finalText = text { + + let range = NSRange(finalText.startIndex..., in: finalText) + + if let regex = try? NSRegularExpression(pattern: regex) { + let maskedText = regex.stringByReplacingMatches(in: finalText, + range: range, + withTemplate: mask) + textField.text = maskedText + } + } + } + @objc public func dismissFieldInput(_ sender: Any?) { resignFirstResponder() } @@ -317,6 +337,10 @@ import UIKit case .password, .secure: textField.isSecureTextEntry = true + case .numberSecure: + textField.isSecureTextEntry = true + textField.keyboardType = .numberPad + case .number: textField.keyboardType = .numberPad @@ -326,17 +350,20 @@ import UIKit case .phone: textField.keyboardType = .phonePad - default: - break + default: break } + // Override the preset keyboard set in type. + if let keyboardType = model.assignKeyboardType() { + textField.keyboardType = keyboardType + } + + textField.accessibilityIdentifier = model.accessibilityIdentifier uiTextFieldDelegate = delegateObject?.uiTextFieldDelegate observingTextFieldDelegate = delegateObject?.observingTextFieldDelegate setupTextFieldToolbar() - if isSelected { - startEditing() - } + if isSelected { startEditing() } } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift index 6f54cb38..ca7543bd 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextEntryFieldModel.swift @@ -16,6 +16,7 @@ case password case secure case number + case numberSecure case email case text case phone @@ -25,15 +26,69 @@ // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { - return "textField" - } + public override class var identifier: String { "textField" } public var placeholder: String? public var enabledTextColor: Color = Color(uiColor: .mvmBlack) public var disabledTextColor: Color = Color(uiColor: .mvmCoolGray3) public var textAlignment: NSTextAlignment = .left + public var keyboardOverride: String? public var type: EntryType? + public var clearTextOnTap: Bool = false + public var displayFormat: String? + public var displayMask: String? + + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + + /// Reads the keyboardOverride set by server and returns the keyboard type associated with it. + func assignKeyboardType() -> UIKeyboardType? { + + guard let keyboardType = keyboardOverride else { return nil } + + var typeInt = 0 + + switch keyboardType { + case "asciiCapable": + typeInt = 1 // Displays a keyboard which can enter ASCII characters + + case "numbersAndPunctuation": + typeInt = 2 // Numbers and assorted punctuation. + + case "URL": + typeInt = 3 // A type optimized for URL entry (shows . / .com prominently). + + case "numberPad": + typeInt = 4 // A number pad with locale-appropriate digits (0-9, ۰-۹, ०-९, etc.). Suitable for PIN entry. + + case "phonePad": + typeInt = 5 // A phone pad (1-9, *, 0, #, with letters under the numbers). + + case "namePhonePad": + typeInt = 6 // A type optimized for entering a person's name or phone number. + + case "emailAddress": + typeInt = 7 // A type optimized for multiple email address entry (shows space @ . prominently). + + case "decimalPad": + typeInt = 8 // A number pad with a decimal point. + + case "twitter": + typeInt = 9 // A type optimized for twitter text entry (easy access to @ #) + + case "webSearch": + typeInt = 10 // A default keyboard type with URL-oriented addition (shows space . prominently). + + case "asciiCapableNumberPad": + typeInt = 11 // A number pad (0-9) that will always be ASCII digits. + + default: + typeInt = 0 // Default type for the current input method. + } + + return UIKeyboardType(rawValue: typeInt) + } //-------------------------------------------------- // MARK: - Keys @@ -44,7 +99,11 @@ case textAlignment case enabledTextColor case disabledTextColor + case keyboardOverride case type + case clearTextOnTap + case displayFormat + case displayMask } //-------------------------------------------------- @@ -56,8 +115,15 @@ let typeContainer = try decoder.container(keyedBy: CodingKeys.self) placeholder = try typeContainer.decodeIfPresent(String.self, forKey: .placeholder) + displayFormat = try typeContainer.decodeIfPresent(String.self, forKey: .displayFormat) + keyboardOverride = try typeContainer.decodeIfPresent(String.self, forKey: .keyboardOverride) + displayMask = try typeContainer.decodeIfPresent(String.self, forKey: .displayMask) type = try typeContainer.decodeIfPresent(EntryType.self, forKey: .type) + if let clearTextOnTap = try typeContainer.decodeIfPresent(Bool.self, forKey: .clearTextOnTap) { + self.clearTextOnTap = clearTextOnTap + } + if let enabledTextColor = try typeContainer.decodeIfPresent(Color.self, forKey: .enabledTextColor) { self.enabledTextColor = enabledTextColor } @@ -76,8 +142,12 @@ var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(placeholder, forKey: .placeholder) try container.encodeIfPresent(textAlignment, forKey: .textAlignment) + try container.encodeIfPresent(type, forKey: .type) + try container.encodeIfPresent(displayFormat, forKey: .displayFormat) + try container.encodeIfPresent(keyboardOverride, forKey: .keyboardOverride) + try container.encodeIfPresent(displayMask, forKey: .displayMask) try container.encode(enabledTextColor, forKey: .enabledTextColor) try container.encode(disabledTextColor, forKey: .disabledTextColor) - try container.encodeIfPresent(type, forKey: .type) + try container.encode(clearTextOnTap, forKey: .clearTextOnTap) } } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift index e08f3843..543469c1 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryField.swift @@ -31,11 +31,11 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele //-------------------------------------------------- public var textViewEntryFieldModel: TextViewEntryFieldModel? { - return model as? TextViewEntryFieldModel + model as? TextViewEntryFieldModel } public override var isEnabled: Bool { - get { return super.isEnabled } + get { super.isEnabled } set (enabled) { super.isEnabled = enabled @@ -53,7 +53,7 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele } public override var showError: Bool { - get { return super.showError } + get { super.showError } set (error) { if error { @@ -68,7 +68,7 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele /// The text of this textView. open override var text: String? { - get { return textViewEntryFieldModel?.text } + get { textViewEntryFieldModel?.text } set { textView.text = newValue textViewEntryFieldModel?.text = newValue @@ -77,7 +77,7 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele /// Placeholder access for the textView. public var placeholder: String? { - get { return textViewEntryFieldModel?.placeholder } + get { textViewEntryFieldModel?.placeholder } set { textView.placeholder = newValue ?? "" textViewEntryFieldModel?.placeholder = newValue @@ -127,7 +127,7 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele /// If you're using a ViewController, you must set this to it public weak var uiTextViewDelegate: UITextViewDelegate? { - get { return textView.delegate } + get { textView.delegate } set { textView.delegate = newValue } } @@ -149,17 +149,17 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele @objc open override func setupFieldContainerContent(_ container: UIView) { container.addSubview(textView) - + topConstraint = textView.topAnchor.constraint(equalTo: container.topAnchor, constant: Padding.Three) leadingConstraint = textView.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: Padding.Three) trailingConstraint = container.trailingAnchor.constraint(equalTo: textView.trailingAnchor, constant: Padding.Three) bottomConstraint = container.bottomAnchor.constraint(equalTo: textView.bottomAnchor, constant: Padding.Three) - + topConstraint?.isActive = true leadingConstraint?.isActive = true trailingConstraint?.isActive = true bottomConstraint?.isActive = true - + heightConstraint = textView.heightAnchor.constraint(equalToConstant: 0) accessibilityElements = [titleLabel, textView, feedbackLabel] } @@ -203,7 +203,7 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele /// Executes on UITextView.textDidEndEditingNotification @objc override func endInputing() { super.endInputing() - + // Don't show error till user starts typing. guard text?.count ?? 0 != 0 else { showError = false @@ -241,6 +241,7 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele textView.isEditable = model.editable textView.textAlignment = model.textAlignment + textView.accessibilityIdentifier = model.accessibilityIdentifier textView.textColor = model.enabled ? model.enabledTextColor.uiColor : model.disabledTextColor.uiColor textView.font = model.fontStyle.getFont() textView.placeholder = model.placeholder ?? "" @@ -252,13 +253,17 @@ class TextViewEntryField: EntryField, UITextViewDelegate, ObservingTextFieldDele case .secure, .password: textView.isSecureTextEntry = true + case .numberSecure: + textView.isSecureTextEntry = true + textView.keyboardType = .numberPad + case .number: textView.keyboardType = .numberPad case .email: textView.keyboardType = .emailAddress - default: break + default: break } /// No point in configuring if the TextView is Read-only. diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift index ce1173ae..42da3ca2 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/TextViewEntryFieldModel.swift @@ -14,9 +14,7 @@ class TextViewEntryFieldModel: TextEntryFieldModel { // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { - return "textView" - } + public override class var identifier: String { "textView" } public var accessibilityText: String? public var fontStyle: Styler.Font = Styler.Font.RegularBodyLarge @@ -25,7 +23,7 @@ class TextViewEntryFieldModel: TextEntryFieldModel { public var placeholderFontStyle: Styler.Font = Styler.Font.RegularMicro public var editable: Bool = true public var showsPlaceholder: Bool = false - + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift index edc37628..f1fd5fed 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Checkbox.swift @@ -25,7 +25,7 @@ import MVMCore var delegateObject: MVMCoreUIDelegateObject? public var checkboxModel: CheckboxModel? { - return model as? CheckboxModel + model as? CheckboxModel } public static let defaultHeightWidth: CGFloat = 18.0 @@ -59,7 +59,7 @@ import MVMCore /// Retrieves ideeal radius value to curve square into a circle. public var cornerRadiusValue: CGFloat { - return bounds.size.height / 2 + bounds.size.height / 2 } /// Action Block called when the switch is selected. @@ -102,23 +102,17 @@ import MVMCore /// Color of the check mark. public var checkColor: UIColor = .mvmBlack { - didSet { - setShapeLayerStrokeColor(checkColor) - } + didSet { setShapeLayerStrokeColor(checkColor) } } /// Border width of the checkbox public var borderWidth: CGFloat = 1 { - didSet { - layer.borderWidth = borderWidth - } + didSet { layer.borderWidth = borderWidth } } /// border color of the Checkbox public var borderColor: UIColor = .mvmBlack { - didSet { - layer.borderColor = borderColor.cgColor - } + didSet { layer.borderColor = borderColor.cgColor } } /** @@ -359,6 +353,7 @@ import MVMCore } override open func accessibilityActivate() -> Bool { + guard isEnabled else { return false } sendActions(for: .touchUpInside) return true } @@ -367,9 +362,7 @@ import MVMCore // MARK: - Molecular //-------------------------------------------------- - open func needsToBeConstrained() -> Bool { - return true - } + open func needsToBeConstrained() -> Bool { true } open override func reset() { super.reset() @@ -396,11 +389,9 @@ import MVMCore } private func performCheckboxAction(with actionModel: ActionModelProtocol, delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) { - if let actionMap = actionModel.toJSON() { - var additionalDatatoUpdate = additionalData ?? [:] - additionalDatatoUpdate[KeySourceModel] = checkboxModel - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalDatatoUpdate, delegateObject: delegateObject) - } + var additionalDataToUpdate = additionalData ?? [:] + additionalDataToUpdate[KeySourceModel] = checkboxModel + MVMCoreActionHandler.shared()?.asyncHandleAction(with: actionModel, additionalData: additionalDataToUpdate, delegateObject: delegateObject) } public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift index bb21da1b..c2d9f14e 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift @@ -16,6 +16,7 @@ import Foundation public static var identifier: String = "checkbox" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var checked: Bool = false public var enabled: Bool = true public var animated: Bool = true @@ -44,6 +45,7 @@ import Foundation private enum CodingKeys: String, CodingKey { case moleculeName + case accessibilityIdentifier case checked case enabled case inverted @@ -69,9 +71,7 @@ import Foundation // MARK: - Methods //-------------------------------------------------- - public func formFieldValue() -> AnyHashable? { - return checked - } + public func formFieldValue() -> AnyHashable? { checked } //-------------------------------------------------- // MARK: - Initializer @@ -89,6 +89,8 @@ import Foundation required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) + if let borderWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .borderWidth) { self.borderWidth = borderWidth } @@ -169,6 +171,7 @@ import Foundation try container.encode(borderWidth, forKey: .borderWidth) try container.encode(checked, forKey: .checked) try container.encode(inverted, forKey: .inverted) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(checkColor, forKey: .checkColor) try container.encodeIfPresent(invertedColor, forKey: .invertedColor) try container.encodeIfPresent(invertedBackgroundColor, forKey: .invertedBackgroundColor) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/Heart.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Heart.swift index 1cf80fc3..e694e194 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/Heart.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Heart.swift @@ -71,6 +71,7 @@ import UIKit //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- + open override func setupView() { super.setupView() addTarget(self, action: #selector(tapAction), for: .touchUpInside) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift index 7411899c..aea065ef 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift @@ -6,15 +6,16 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- + public static var identifier: String = "heart" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var isActive: Bool = false public var activeColor: Color = Color(uiColor: .mvmRed) public var inActiveColor: Color = Color(uiColor: .clear) @@ -24,9 +25,11 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case isActive case activeColor case inActiveColor @@ -43,13 +46,18 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { if let isActive = try typeContainer.decodeIfPresent(Bool.self, forKey: .isActive) { self.isActive = isActive } + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) + if let activeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .activeColor) { self.activeColor = activeColor } + if let inActiveColor = try typeContainer.decodeIfPresent(Color.self, forKey: .inActiveColor) { self.inActiveColor = inActiveColor } + if let action: ActionModelProtocol = try typeContainer.decodeModelIfPresent(codingKey: .action) { self.action = action } @@ -61,6 +69,7 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(isActive, forKey: .isActive) try container.encode(activeColor, forKey: .activeColor) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBox.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBox.swift index d4fa0232..1d8c6960 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBox.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBox.swift @@ -6,9 +6,12 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation open class RadioBox: Control, MFButtonProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public let label = Label(fontStyle: .RegularBodySmall) public let subTextLabel = Label(fontStyle: .RegularMicro) public var isOutOfStock = false @@ -26,22 +29,20 @@ open class RadioBox: Control, MFButtonProtocol { var additionalData: [AnyHashable: Any]? public var radioBoxModel: RadioBoxModel? { - return model as? RadioBoxModel + model as? RadioBoxModel } public override var isSelected: Bool { - didSet { - updateAccessibility() - } + didSet { updateAccessibility() } } public override var isEnabled: Bool { - didSet { - updateAccessibility() - } + didSet { updateAccessibility() } } + //-------------------------------------------------- // MARK: - MVMCoreViewProtocol + //-------------------------------------------------- open override func updateView(_ size: CGFloat) { super.updateView(size) @@ -75,8 +76,6 @@ open class RadioBox: Control, MFButtonProtocol { isAccessibilityElement = true } - // MARK: - MoleculeViewProtocol - open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) guard let model = model as? RadioBoxModel else { return } @@ -99,7 +98,9 @@ open class RadioBox: Control, MFButtonProtocol { accentColor = .mvmRed } + //-------------------------------------------------- // MARK: - State Handling + //-------------------------------------------------- open override func draw(_ layer: CALayer, in ctx: CGContext) { // Draw the strikethrough @@ -213,21 +214,29 @@ open class RadioBox: Control, MFButtonProtocol { return mask } + //-------------------------------------------------- // MARK: - Accessibility + //-------------------------------------------------- + public func updateAccessibility() { + var message = "" + if let labelText = label.text, label.hasText { message += labelText + ", " } + if let subLabelText = subTextLabel.text, subTextLabel.hasText { message += subLabelText + ", " } - accessibilityLabel = message + accessibilityLabel = message accessibilityTraits = .button + if isSelected { accessibilityTraits.insert(.selected) } + if !isEnabled { accessibilityTraits.insert(.notEnabled) } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxCollectionViewCell.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxCollectionViewCell.swift index 314a2453..d6b9214d 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxCollectionViewCell.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxCollectionViewCell.swift @@ -6,10 +6,17 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation open class RadioBoxCollectionViewCell: CollectionViewCell { + //-------------------------------------------------- + // MARK: - Outlets + //-------------------------------------------------- + public let radioBox = RadioBox() + //-------------------------------------------------- + // MARK: - Lifecycle + //-------------------------------------------------- + open override func reset() { super.reset() backgroundColor = .clear diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift index 923a65e0..5d5443d0 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift @@ -6,12 +6,16 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers public class RadioBoxModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "radioBox" public var text: String public var subText: String? public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var selectedAccentColor: Color? public var selected: Bool = false public var enabled: Bool = true @@ -19,12 +23,17 @@ import Foundation public var fieldValue: String? public var action: ActionModelProtocol? + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case text case subText case selectedAccentColor case backgroundColor + case accessibilityIdentifier case selected case enabled case strikethrough @@ -32,18 +41,26 @@ import Foundation case action } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + 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) selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) + 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 } @@ -59,6 +76,7 @@ import Foundation try container.encodeIfPresent(subText, forKey: .subText) try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(selected, forKey: .selected) try container.encode(enabled, forKey: .enabled) try container.encode(strikethrough, forKey: .strikethrough) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift index 0b17061e..4a596a35 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxes.swift @@ -57,11 +57,12 @@ open class RadioBoxes: View { 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) + guard let model = model as? RadioBoxesModel else { return } + boxes = model.boxes + FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) - backgroundColor = radioBoxesModel.backgroundColor?.uiColor + backgroundColor = model.backgroundColor?.uiColor + registerCells() setHeight() collectionView.reloadData() @@ -168,4 +169,3 @@ extension RadioBoxes: UICollectionViewDelegate { cell.updateAccessibility() } } - diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift index 5b568910..bd1f99d1 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift @@ -6,17 +6,25 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers public class RadioBoxesModel: MoleculeModelProtocol, FormFieldProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "radioBoxes" public var boxes: [RadioBoxModel] public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var selectedAccentColor: Color? public var boxesColor: Color? public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + /// 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 @@ -25,20 +33,30 @@ import Foundation return selectedBox?.fieldValue ?? selectedBox?.text } + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case selectedAccentColor case backgroundColor + case accessibilityIdentifier case boxesColor case boxes case fieldKey case groupName } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + 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) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) boxesColor = try typeContainer.decodeIfPresent(Color.self, forKey: .boxesColor) boxes = try typeContainer.decode([RadioBoxModel].self, forKey: .boxes) fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) @@ -54,6 +72,7 @@ import Foundation try container.encode(boxes, forKey: .boxes) try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encode(groupName, forKey: .groupName) } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift index bd720be8..1098ad3d 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift @@ -15,9 +15,7 @@ import UIKit //-------------------------------------------------- public var diameter: CGFloat = 30 { - didSet { - widthConstraint?.constant = diameter - } + didSet { widthConstraint?.constant = diameter } } @objc public override var isSelected: Bool { @@ -33,12 +31,10 @@ import UIKit var additionalData: [AnyHashable: Any]? public var radioModel: RadioButtonModel? { - return model as? RadioButtonModel + model as? RadioButtonModel } - lazy public var radioGroupName: String? = { - return radioModel?.fieldKey - }() + lazy public var radioGroupName: String? = { radioModel?.fieldKey }() lazy public var radioButtonSelectionHelper: RadioButtonSelectionHelper? = { @@ -95,33 +91,34 @@ import UIKit if !isEnabled { return } + let wasPreviouslySelected = isSelected if let radioButtonModel = radioButtonSelectionHelper { radioButtonModel.selected(self) } else { isSelected = !isSelected } + if let radioModel = radioModel, let actionModel = radioModel.action, isSelected, !wasPreviouslySelected { Button.performButtonAction(with: actionModel, button: self, delegateObject: delegateObject, additionalData: additionalData, sourceModel: radioModel) } + _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate) setNeedsDisplay() } - public func isValidField() -> Bool { - return isSelected - } + public func isValidField() -> Bool { isSelected } public func formFieldName() -> String? { - return radioModel?.fieldKey + radioModel?.fieldKey } public func formFieldGroupName() -> String? { - return radioModel?.fieldKey + radioModel?.fieldKey } public func formFieldValue() -> AnyHashable? { - return radioModel?.fieldValue + radioModel?.fieldValue } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift index 7d50cf56..239a8d41 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation import MVMCore @@ -17,6 +16,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { public static var identifier: String = "radioButton" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var state: Bool = false public var enabled: Bool = true @@ -35,6 +35,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case state case enabled case fieldValue @@ -56,9 +57,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { // MARK: - Validation //-------------------------------------------------- - public func formFieldValue() -> AnyHashable? { - return fieldValue - } + public func formFieldValue() -> AnyHashable? { fieldValue } //-------------------------------------------------- // MARK: - Codec @@ -76,6 +75,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { } backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) baseValue = state fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) @@ -89,6 +89,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(state, forKey: .state) try container.encode(enabled, forKey: .enabled) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift index 4765766b..a5000284 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonSelectionHelper.swift @@ -6,8 +6,6 @@ // Copyright © 2019 Verizon Wireless. All rights reserved. // -import Foundation - @objcMembers public class RadioButtonSelectionHelper: FormFieldProtocol { //-------------------------------------------------- @@ -77,7 +75,5 @@ import Foundation // MARK: - FormValidationFormFieldProtocol extension RadioButtonSelectionHelper { - public func formFieldValue() -> AnyHashable? { - return selectedRadioButtonModel?.fieldValue - } + public func formFieldValue() -> AnyHashable? { selectedRadioButtonModel?.fieldValue } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift index 70f2a459..e80f8813 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatch.swift @@ -77,6 +77,7 @@ open class RadioSwatch: Control, MFButtonProtocol { //------------------------------------------------------ // MARK: - State Handling //------------------------------------------------------ + open override func draw(_ layer: CALayer, in ctx: CGContext) { //Draw the swatch circleLayer?.removeFromSuperlayer() diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift index 4a9efb4b..96edb174 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchCollectionViewCell.swift @@ -6,7 +6,7 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation + open class RadioSwatchCollectionViewCell: CollectionViewCell { public let radioSwatch = RadioSwatch() diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift index 7acb3999..c961e306 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift @@ -6,11 +6,15 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers public class RadioSwatchModel: MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "radioSwatch" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var color: Color = Color(uiColor: .mvmBlue) public var text: String? public var selected: Bool = false @@ -19,9 +23,14 @@ import Foundation public var fieldValue: String? public var action: ActionModelProtocol? + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case color case text case selected @@ -31,22 +40,33 @@ import Foundation case action } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) + if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) { self.color = color } + text = try typeContainer.decodeIfPresent(String.self, forKey: .text) + if let selected = try typeContainer.decodeIfPresent(Bool.self, forKey: .selected) { self.selected = selected } + if let enabled = try typeContainer.decodeIfPresent(Bool.self, forKey: .enabled) { self.enabled = enabled } + if let strikethrough = try typeContainer.decodeIfPresent(Bool.self, forKey: .strikethrough) { self.strikethrough = strikethrough } + fieldValue = try typeContainer.decodeIfPresent(String.self, forKey: .fieldValue) action = try typeContainer.decodeModelIfPresent(codingKey: .action) } @@ -55,6 +75,7 @@ import Foundation var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(color, forKey: .color) try container.encodeIfPresent(text, forKey: .text) try container.encode(selected, forKey: .selected) @@ -64,5 +85,3 @@ import Foundation try container.encodeModelIfPresent(action, forKey: .action) } } - - diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift index 6d4f93d3..5a5b6836 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatches.swift @@ -55,9 +55,9 @@ open class RadioSwatches: View { super.set(with: model, delegateObject, additionalData) self.delegateObject = delegateObject - guard let radioSwatchesModel = model as? RadioSwatchesModel else { return } - swatches = radioSwatchesModel.swatches - FormValidator.setupValidation(for: radioSwatchesModel, delegate: delegateObject?.formHolderDelegate) + guard let model = model as? RadioSwatchesModel else { return } + swatches = model.swatches + FormValidator.setupValidation(for: model, delegate: delegateObject?.formHolderDelegate) collectionView.reloadData() } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift index db8e5d5d..d44b631a 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift @@ -6,16 +6,24 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers public class RadioSwatchesModel: MoleculeModelProtocol, FormFieldProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public static var identifier: String = "radioSwatches" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var swatches: [RadioSwatchModel] public var fieldKey: String? public var groupName: String = FormValidator.defaultGroupName public var baseValue: AnyHashable? + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + /// Returns the fieldValue of the selected swatch, otherwise the text of selected swatch. public func formFieldValue() -> AnyHashable? { let selectedSwatch = swatches.first { (swatch) -> Bool in @@ -24,17 +32,27 @@ import Foundation return selectedSwatch?.fieldValue ?? selectedSwatch?.text } + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case swatches case fieldKey case groupName } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) swatches = try typeContainer.decode([RadioSwatchModel].self, forKey: .swatches) fieldKey = try typeContainer.decodeIfPresent(String.self, forKey: .fieldKey) if let groupName = try typeContainer.decodeIfPresent(String.self, forKey: .groupName) { @@ -47,6 +65,7 @@ import Foundation var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(swatches, forKey: .swatches) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) try container.encode(groupName, forKey: .groupName) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Toggle.swift b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift similarity index 93% rename from MVMCoreUI/Atomic/Atoms/Views/Toggle.swift rename to MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift index 5712695b..304b23c4 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Toggle.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/Toggle.swift @@ -40,7 +40,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) /// Executes logic before state change. If false, then toggle state will not change and the didToggleAction will not execute. public var shouldToggleAction: ActionBlockConfirmation? = { - return { return true } + return { true } }() // Sizes are from InVision design specs. @@ -69,9 +69,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) /// Simple means to prevent user interaction with the toggle. public var isLocked: Bool = false { - didSet { - isUserInteractionEnabled = !isLocked - } + didSet { isUserInteractionEnabled = !isLocked } } /// The state on the toggle. Default value: false. @@ -109,7 +107,7 @@ public typealias ActionBlockConfirmation = () -> (Bool) } public var toggleModel: ToggleModel? { - return model as? ToggleModel + model as? ToggleModel } //-------------------------------------------------- @@ -392,21 +390,17 @@ public typealias ActionBlockConfirmation = () -> (Bool) accessibilityLabel = accessibileString } - let actionMap = model.action?.toJSON() - let alternateActionMap = model.alternateAction?.toJSON() let additionalDataWithSource = additionalData.dictionaryAdding(key: KeySourceModel, value: model) - if actionMap != nil || alternateActionMap != nil { + if model.action != nil || model.alternateAction != nil { didToggleAction = { [weak self] in guard let self = self else { return } if self.isOn { - if actionMap != nil { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalDataWithSource, delegateObject: delegateObject) + if let action = model.action { + MVMCoreActionHandler.shared()?.asyncHandleAction(with: action, additionalData: additionalDataWithSource, delegateObject: delegateObject) } } else { - if alternateActionMap != nil { - MVMCoreActionHandler.shared()?.handleAction(with: alternateActionMap, additionalData: additionalDataWithSource, delegateObject: delegateObject) - } else if actionMap != nil { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalDataWithSource, delegateObject: delegateObject) + if let action = model.alternateAction ?? model.action { + MVMCoreActionHandler.shared()?.asyncHandleAction(with: action, additionalData: additionalDataWithSource, delegateObject: delegateObject) } } } @@ -414,18 +408,14 @@ public typealias ActionBlockConfirmation = () -> (Bool) } public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return Self.getContainerHeight() + Self.getContainerHeight() } } // MARK: - MVMCoreUIViewConstrainingProtocol extension Toggle { - public func needsToBeConstrained() -> Bool { - return true - } + public func needsToBeConstrained() -> Bool { true } - public func horizontalAlignment() -> UIStackView.Alignment { - return .trailing - } + public func horizontalAlignment() -> UIStackView.Alignment { .trailing } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift similarity index 93% rename from MVMCoreUI/Atomic/Atoms/Views/ToggleModel.swift rename to MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index b39003d2..4990df73 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -6,8 +6,6 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import UIKit - public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableModelProtocol { //-------------------------------------------------- @@ -15,6 +13,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableMo //-------------------------------------------------- public static var identifier: String = "toggle" + public var accessibilityIdentifier: String? public var backgroundColor: Color? public var state: Bool = false public var animated: Bool = true @@ -42,6 +41,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableMo case enabled case action case backgroundColor + case accessibilityIdentifier case alternateAction case accessibilityText case onTintColor @@ -56,9 +56,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableMo // MARK: - Methods //-------------------------------------------------- - public func formFieldValue() -> AnyHashable? { - return state - } + public func formFieldValue() -> AnyHashable? { state } //-------------------------------------------------- // MARK: - Initializer @@ -91,6 +89,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableMo action = try typeContainer.decodeModelIfPresent(codingKey: .action) alternateAction = try typeContainer.decodeModelIfPresent(codingKey: .alternateAction) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) if let onTintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .onTintColor) { self.onTintColor = onTintColor @@ -120,6 +119,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol, EnableableMo public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModelIfPresent(action, forKey: .action) try container.encodeModelIfPresent(alternateAction, forKey: .alternateAction) try container.encode(moleculeName, forKey: .moleculeName) diff --git a/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift index 523d83a6..c4375d53 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift @@ -16,6 +16,7 @@ import Foundation public static var identifier: String = "dashLine" public var backgroundColor: Color? + public var accessibilityIdentifier: String? public var dashColor: Color = Color(uiColor: .mvmCoolGray3) public var dashColor_inverted: Color = Color(uiColor: .mvmWhite) public var isHidden: Bool = false @@ -36,6 +37,7 @@ import Foundation private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case dashColor_inverted case dashColor case isHidden @@ -57,6 +59,7 @@ import Foundation } backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) if let isHidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .isHidden) { self.isHidden = isHidden @@ -69,5 +72,6 @@ import Foundation try container.encode(dashColor, forKey: .dashColor) try container.encode(isHidden, forKey: .isHidden) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 3f485099..7e902d435 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -38,7 +38,7 @@ public typealias ActionBlock = () -> () } public var getRange: NSRange { - return NSRange(location: 0, length: text?.count ?? 0) + NSRange(location: 0, length: text?.count ?? 0) } //------------------------------------------------------ @@ -209,7 +209,7 @@ public typealias ActionBlock = () -> () /// Default @objc open class func label() -> Label { - return Label(frame: .zero) + Label(frame: .zero) } //------------------------------------------------------ @@ -381,9 +381,7 @@ public typealias ActionBlock = () -> () } case let actionAtt as LabelAttributeActionModel: addTappableLinkAttribute(range: NSRange(location: range.location, length: range.length)) { - if let data = try? actionAtt.action.encode(using: JSONEncoder()), let actionMap = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.init()) as? [AnyHashable: Any] { - MVMCoreActionHandler.shared()?.handleAction(with: actionMap, additionalData: additionalData, delegateObject: delegateObject) - } + MVMCoreActionHandler.shared()?.asyncHandleAction(with: actionAtt.action, additionalData: additionalData, delegateObject: delegateObject) } addActionAttributes(range: range, string: attributedString) @@ -794,17 +792,11 @@ extension Label { accessibilityTraits = .staticText } - public func needsToBeConstrained() -> Bool { - return true - } + public func needsToBeConstrained() -> Bool { true } - public func horizontalAlignment() -> UIStackView.Alignment { - return .leading - } + public func horizontalAlignment() -> UIStackView.Alignment { .leading } - public func copyBackgroundColor() -> Bool { - return true - } + public func copyBackgroundColor() -> Bool { true } } // MARK: - Multi-Link Functionality @@ -916,6 +908,7 @@ extension UITapGestureRecognizer { if label.makeWholeViewClickable { return true } + guard let abstractContainer = label.abstractTextContainer() else { return false } let textContainer = abstractContainer.0 let layoutManager = abstractContainer.1 @@ -990,6 +983,7 @@ extension Label { } } } + return false } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift index f9920dcc..1ecbe874 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeActionModel.swift @@ -12,9 +12,7 @@ open class LabelAttributeActionModel: LabelAttributeModel { // MARK: - Properties //-------------------------------------------------- - override public class var identifier: String { - return "action" - } + override public class var identifier: String { "action" } var action: ActionModelProtocol diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift index b86251c3..94579d02 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeColorModel.swift @@ -12,9 +12,7 @@ // MARK: - Properties //-------------------------------------------------- - override public class var identifier: String { - return "color" - } + override public class var identifier: String { "color" } var textColor: Color? diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift index 59dbac3d..72d57e40 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeFontModel.swift @@ -12,9 +12,7 @@ // MARK: - Properties //-------------------------------------------------- - override public class var identifier: String { - return "font" - } + override public class var identifier: String { "font" } var style: Styler.Font? var name: String? diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift index 70cd90fd..12e8d68d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeImageModel.swift @@ -12,9 +12,7 @@ class LabelAttributeImageModel: LabelAttributeModel { // MARK: - Properties //-------------------------------------------------- - override public class var identifier: String { - return "image" - } + override public class var identifier: String { "image" } var size: CGFloat? var name: String? diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift index ae5f15b7..7df4097a 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeModel.swift @@ -12,20 +12,14 @@ // MARK: - Properties //-------------------------------------------------- - public static var categoryName: String { - return "\(LabelAttributeModel.self)" - } + public static var categoryName: String { "\(LabelAttributeModel.self)" } - public static var categoryCodingKey: String { - return "type" - } + public static var categoryCodingKey: String { "type" } - public class var identifier: String { - return "" - } + public class var identifier: String { "" } var type: String { - get { return Self.identifier } + get { Self.identifier } } var location: Int diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeStrikeThroughModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeStrikeThroughModel.swift index e84c1804..83744a68 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeStrikeThroughModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeStrikeThroughModel.swift @@ -12,9 +12,7 @@ // MARK: - Properties //-------------------------------------------------- - override public class var identifier: String { - return "strikethrough" - } + override public class var identifier: String { "strikethrough" } //-------------------------------------------------- // MARK: - Initializer diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift index a3adbe00..f3578418 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelAttributeUnderlineModel.swift @@ -14,9 +14,7 @@ import UIKit // MARK: - Properties //-------------------------------------------------- - override public class var identifier: String { - return "underline" - } + override public class var identifier: String { "underline" } /// This returns the NSUnderlineStyle used in NSAttributedValue. If there is a pattern, it will return /// a new NSUnderlineStyle derived from the bitmask of style | pattern. diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index 0ad4c16b..8d8ef49e 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -21,7 +21,8 @@ public var addSizeConstraintsForAspectRatio = true public var shouldNotifyDelegateOnUpdate = true - + public var shouldNotifyDelegateOnDefaultSizeChange = false + // Allows for a view to hardcode which height to use if there is none in the json. var imageWidth: CGFloat? var imageHeight: CGFloat? @@ -228,13 +229,13 @@ let widthWillChange = !MVMCoreGetterUtility.cgfequal(widthConstraint?.constant ?? 0, width ?? 0) let heightWillChange = !MVMCoreGetterUtility.cgfequal(heightConstraint?.constant ?? 0, height ?? 0) - let sizeWillChange = (width == nil || height == nil) && !(size?.equalTo(imageView.image?.size ?? CGSize.zero) ?? false) + let sizeWillChange = shouldNotifyDelegateOnDefaultSizeChange && (width == nil || height == nil) && !(size?.equalTo(imageView.image?.size ?? CGSize.zero) ?? false) let heightChangeFromSpinner = (heightConstraint?.isActive ?? false) ? false : ((height ?? size?.height) ?? 0) < loadingSpinnerHeightConstraint?.constant ?? CGFloat.leastNormalMagnitude return widthWillChange || heightWillChange || sizeWillChange || heightChangeFromSpinner } // Constrains the image view to be the size provided. Used to size it to the image to fix aspect fit defect. - func addConstraints(width: NSNumber?, height: NSNumber?, size: CGSize?) { + func addConstraints(width: CGFloat?, height: CGFloat?, size: CGSize?) { widthConstraint?.isActive = false heightConstraint?.isActive = false @@ -242,15 +243,15 @@ guard addSizeConstraintsForAspectRatio else { return } if let width = width, let height = height { - setHeight(height.cgfloat()) - setWidth(width.cgfloat()) + setHeight(height) + setWidth(width) } else if let width = width, let size = size { - setWidth(width.cgfloat()) + setWidth(width) heightConstraint = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: size.height/size.width) heightConstraint?.priority = UILayoutPriority(rawValue: 900) heightConstraint?.isActive = true } else if let height = height, let size = size { - setHeight(height.cgfloat()) + setHeight(height) widthConstraint = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: size.width/size.height) widthConstraint?.priority = UILayoutPriority(rawValue: 900) widthConstraint?.isActive = true @@ -288,7 +289,7 @@ if shouldLoadImage(withName: imageModel.image, width: width, height: height) { imageView.image = nil imageView.animatedImage = nil - loadImage(withName: imageModel.image, format: imageModel.imageFormat, width: width as NSNumber?, height: height as NSNumber?, customFallbackImage: imageModel.fallbackImage, localBundle: imageModel.localBundle) + loadImage(withName: imageModel.image, format: imageModel.imageFormat, width: width, height: height, customFallbackImage: imageModel.fallbackImage, localBundle: imageModel.localBundle) } if let contentMode = imageModel.contentMode { @@ -309,13 +310,13 @@ // MARK: - Load Methods //-------------------------------------------------- - public func loadImage(withName imageName: String?, format: String? = nil, width: NSNumber? = nil, height: NSNumber? = nil, customFallbackImage: String? = nil, allowServerParameters: Bool = false, localBundle: Bundle? = nil, completionHandler: MVMCoreGetImageBlock? = nil) { + public func loadImage(withName imageName: String?, format: String? = nil, width: CGFloat? = nil, height: CGFloat? = nil, customFallbackImage: String? = nil, allowServerParameters: Bool = false, localBundle: Bundle? = nil, completionHandler: MVMCoreGetImageBlock? = nil) { let completionBlock = completionHandler ?? defaultCompletionBlock() MVMCoreDispatchUtility.performBlock(onMainThread: { [unowned self] in self.currentImageName = imageName - self.currentImageWidth = width?.cgfloat() - self.currentImageHeight = height?.cgfloat() + self.currentImageWidth = width + self.currentImageHeight = height if MVMCoreCache.isHostedImage(imageName) { self.loadingSpinner.resumeSpinnerAfterDelay() self.loadingSpinnerHeightConstraint?.constant = self.spinnerHeight @@ -338,9 +339,9 @@ let fallbackImageName = customFallbackImage ?? MVMCoreUIUtility.localizedImageName("fallback") if let format = format, format.lowercased().contains("gif") { // Gifs aren't supported by default and need special handling - MVMCoreCache.shared()?.getGif(imageName, useWidth: width != nil, widthForS7: width?.intValue ?? 0, useHeight: height != nil, heightForS7: height?.intValue ?? 0, format: format, localFallbackImageName: fallbackImageName, allowServerQueryParameters: allowServerParameters, localBundle: localBundle, completionHandler: finishedLoadingBlock) + MVMCoreCache.shared()?.getGif(imageName, useWidth: width != nil, widthForS7: Int(width ?? 0), useHeight: height != nil, heightForS7: Int(height ?? 0), format: format, localFallbackImageName: fallbackImageName, allowServerQueryParameters: allowServerParameters, localBundle: localBundle, completionHandler: finishedLoadingBlock) } else { - MVMCoreCache.shared()?.getImage(imageName, useWidth: width != nil, widthForS7: width?.intValue ?? 0, useHeight: height != nil, heightForS7: height?.intValue ?? 0, format: format, localFallbackImageName: fallbackImageName, allowServerQueryParameters: allowServerParameters, localBundle: localBundle, completionHandler: finishedLoadingBlock) + MVMCoreCache.shared()?.getImage(imageName, useWidth: width != nil, widthForS7: Int(width ?? 0), useHeight: height != nil, heightForS7: Int(height ?? 0), format: format, localFallbackImageName: fallbackImageName, allowServerQueryParameters: allowServerParameters, localBundle: localBundle, completionHandler: finishedLoadingBlock) } }) } @@ -409,6 +410,6 @@ } public func loadImage(withName imageName: String?, format: String?, width: NSNumber?, height: NSNumber?, customFallbackImage: String?, completionHandler: @escaping MVMCoreGetImageBlock) { - loadImage(withName: imageName, format: format, width: width, height: height, customFallbackImage: customFallbackImage, allowServerParameters: false, completionHandler: completionHandler) + loadImage(withName: imageName, format: format, width: width?.cgfloat(), height: height?.cgfloat(), customFallbackImage: customFallbackImage, allowServerParameters: false, completionHandler: completionHandler) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/MultiProgress.swift b/MVMCoreUI/Atomic/Atoms/Views/MultiProgress.swift index 945229c7..2894ea1c 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/MultiProgress.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/MultiProgress.swift @@ -17,7 +17,7 @@ import UIKit private let stack = Stack() var multiProgressModel: MultiProgressBarModel? { - get { return model as? MultiProgressBarModel } + get { model as? MultiProgressBarModel } } var roundedCorners: Bool = false { @@ -85,7 +85,7 @@ import UIKit //-------------------------------------------------- /// Creates the bars - open func set(with progressList: Array, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + open func set(with progressList: [SingleProgressBarModel], _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { stack.removeAllItemViews() guard let stackModel = stack.stackModel else { return } @@ -103,7 +103,6 @@ import UIKit stack.set(with: stackModel, delegateObject, additionalData) } - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) @@ -115,6 +114,6 @@ import UIKit } public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return (model as? MultiProgressBarModel)?.thickness ?? 8 + (model as? MultiProgressBarModel)?.thickness ?? 8 } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video.swift b/MVMCoreUI/Atomic/Atoms/Views/Video.swift new file mode 100644 index 00000000..baee04f6 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/Video.swift @@ -0,0 +1,108 @@ +// +// Video.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 1/26/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// +import AVKit + +open class Video: View { + public let videoViewController = AVPlayerViewController() + private weak var containingView: UIView? + + /// Used to track the state and respond.. + private var stateKVOToken: NSKeyValueObservation? + + open override func setupView() { + super.setupView() + videoViewController.view.translatesAutoresizingMaskIntoConstraints = false + addSubview(videoViewController.view) + NSLayoutConstraint.constraintPinSubview(toSuperview: videoViewController.view) + videoViewController.videoGravity = .resizeAspectFill + } + + /// Checks if the video is visible in the molecule delegate + open func isVisibleInDelegate() -> Bool { + guard let containingView = containingView else { return true } + return isVisible(in: containingView) + } + + /// Checks if the video is visible in the passed in view + open func isVisible(in view: UIView) -> Bool { + return MVMCoreUIUtility.isView(self, visibleIn: view) + } + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + /// Detach the view from it's previous model before setting. + (self.model as? VideoModel)?.view = nil + containingView = (delegateObject?.moleculeDelegate as? UIViewController)?.view + super.set(with: model, delegateObject, additionalData) + + guard let model = model as? VideoModel else { return } + if let controller = delegateObject?.moleculeDelegate as? UIViewController { + controller.addChild(videoViewController) + videoViewController.didMove(toParent: controller) + } + videoViewController.showsPlaybackControls = model.showControls + videoViewController.player = model.videoDataManager.player + addStateObserver() + model.addVisibilityHalting(for: self, delegateObject: delegateObject) + + switch (model.videoDataManager.videoState) { + case .none: + // Begin loading the video + model.videoDataManager.loadVideo() + case .loaded: + guard isVisibleInDelegate() else { return } + // Video loaded, unhalt it if necessary. + model.halted = false + default: + break + } + } + + /// Listens and responds to video loading state changes. + private func addStateObserver() { + removeStateObserver() + + guard stateKVOToken == nil, + let model = model as? VideoModel else { return } + + // To know when the video player item is done loading. + stateKVOToken = + model.videoDataManager.observe(\.videoState) { [weak self] (item, change) in + guard let self = self, + let model = self.model as? VideoModel, + item == model.videoDataManager else { return } + + switch item.videoState { + case .loaded: + // Setting videoController's player must be in the main thread + MVMCoreDispatchUtility.performSyncBlock(onMainThread: { + // Play the video + self.videoViewController.player = item.player + if !model.halted && model.autoPlay && self.isVisibleInDelegate() { + item.player?.play() + UIAccessibility.post(notification: .screenChanged, argument: self) + } + }) + case .failed: + if let errorObject = item.loadFailedError { + MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) + } + default: + break + } + } + } + + private func removeStateObserver() { + stateKVOToken?.invalidate() + stateKVOToken = nil + } + + deinit { + removeStateObserver() + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift new file mode 100644 index 00000000..a4ba9235 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoDataManager.swift @@ -0,0 +1,141 @@ +// +// VideoDataManager.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 1/26/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation +import AVFoundation + +@objcMembers open class VideoDataManager: NSObject { + + /// The state of the video. + @objc public enum VideoState: Int { + case none + case loading + case loaded + case failed + } + + public let videoURLString: String + public var player: AVPlayer? + + // Thread Safe video state handling. + private var _videoState = VideoState.none + private let videoStatueQueue = DispatchQueue(label: "com.vzw.mvmcoreui.videoDataManager.state", attributes: .concurrent) + + /// The state of the video. Use KVO to listen for state changes. + @objc public var videoState: VideoState { + get { + var state = VideoState.none + videoStatueQueue.sync { + state = _videoState + } + return state + } + set { + willChangeValue(for: \.videoState) + videoStatueQueue.async(flags: .barrier) { + self._videoState = newValue + } + didChangeValue(for: \.videoState) + } + } + + /// Set when the state is set to failed. Follows the same pattern as apple's AVPlayerItem + public var loadFailedError: MVMCoreErrorObject? + + private var kvoToken: NSKeyValueObservation? + + private var memoryWarningListener: Any? + + public init(with videoURLString: String) { + self.videoURLString = videoURLString + super.init() + self.addMemoryWarningListener() + } + + public func loadVideo() { + guard videoState != .loading else { return } + removeVideoObserver() + player = nil + videoState = .loading + + //Asset loading needs time, calling async method. by tracking asset's propety "duration", if we get the value of duration, we can treat asset load successfully. + let tracksKey = "duration" + MVMCoreCache.shared()?.playerAsset(fromFileName: videoURLString, trackKeys: [tracksKey], onComplete: { [weak self] (asset, fileName, errorObject) in + guard let asset = asset else { + self?.loadFailedError = errorObject + self?.videoState = .failed + return + } + + var error: NSError? = nil + let tracksStatus = asset.statusOfValue(forKey: tracksKey, error: &error) + switch tracksStatus { + case .loaded: + //When Assets load successfully, we create playerItem and add playerItem into AVPlayer + self?.player = AVPlayer(playerItem: AVPlayerItem(asset: asset)) + self?.addObserverToPlayerItem() + case .failed: + //Asset load fail + //Since checking asset status here, no need to check player.currenItem.asset's media tracks when play button is clicked anymore. + if let error = error, + let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: (asset as? AVURLAsset)?.url.absoluteString ?? self?.videoURLString) { + self?.loadFailedError = errorObject + } + self?.videoState = .failed + default: + break + } + }) + } + + private func addObserverToPlayerItem() { + removeVideoObserver() + + // To know when the video player item is done loading. + guard kvoToken == nil else { return } + kvoToken = player?.currentItem?.observe(\.status) { [weak self] (item, change) in + guard item == self?.player?.currentItem else { return } + switch item.status { + case .readyToPlay: + self?.videoState = .loaded + case .failed: + if let error = item.error, + let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: (item.asset as? AVURLAsset)?.url.absoluteString ?? self?.videoURLString) { + self?.loadFailedError = errorObject + } + self?.videoState = .failed + default: + break + } + } + } + + private func removeVideoObserver() { + kvoToken?.invalidate() + kvoToken = nil + } + + private func addMemoryWarningListener() { + memoryWarningListener = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in + self?.removeVideoObserver() + self?.player = nil + self?.videoState = .none + } + } + + private func removeMemoryWarningListener() { + guard let observer = memoryWarningListener else { return } + NotificationCenter.default.removeObserver(observer) + memoryWarningListener = nil + } + + deinit { + removeVideoObserver() + removeMemoryWarningListener() + } +} diff --git a/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift new file mode 100644 index 00000000..1d079599 --- /dev/null +++ b/MVMCoreUI/Atomic/Atoms/Views/VideoModel.swift @@ -0,0 +1,167 @@ +// +// VideoModel.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 1/26/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +open class VideoModel: MoleculeModelProtocol { + public static var identifier = "video" + public var backgroundColor: Color? + public var video: String + public var showControls = false + public var autoPlay = true + public var alwaysReset = false + weak public var view: Video? + + /// When the video is halted because it is no longer visible + public var halted: Bool = false { + didSet { + guard halted != oldValue, + videoDataManager.videoState == .loaded else { return } + if halted { + videoDataManager.player?.pause() + } else { + if alwaysReset { + // Always start video at the beginning. + videoDataManager.player?.seek(to: .zero) + } + if autoPlay { + videoDataManager.player?.play() + } + } + } + } + + /// Keeps a reference to the video data. + public var videoDataManager: VideoDataManager + + private weak var visibleBehavior: PageVisibilityClosureBehavior? + private weak var scrollBehavior: PageScrolledClosureBehavior? + private var activeListener: Any? + private var resignActiveListener: Any? + + private enum CodingKeys: String, CodingKey { + case moleculeName + case video + case showControls + case autoPlay + case alwaysReset + } + + public init(_ video: String) { + self.video = video + videoDataManager = VideoDataManager(with: video) + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + video = try typeContainer.decode(String.self, forKey:.video) + if let showControls = try typeContainer.decodeIfPresent(Bool.self, forKey: .showControls) { + self.showControls = showControls + } + if let autoPlay = try typeContainer.decodeIfPresent(Bool.self, forKey: .autoPlay) { + self.autoPlay = autoPlay + } + if let alwaysReset = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysReset) { + self.alwaysReset = alwaysReset + } + videoDataManager = VideoDataManager(with: video) + } + + open func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(video, forKey: .video) + try container.encode(showControls, forKey: .showControls) + try container.encode(autoPlay, forKey: .autoPlay) + try container.encode(alwaysReset, forKey: .alwaysReset) + } + + open func addVisibilityHalting(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + self.view = view + halted = false + addVisibleBehavior(for: view, delegateObject: delegateObject) + addScrollBehavior(for: view, delegateObject: delegateObject) + addActiveListener(for: view, delegateObject: delegateObject) + } + + /// Adds a behavior to pause the video on page hidden behavior and unpause if necessary on page shown. + open func addVisibleBehavior(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + + let onShow = { [weak self] in + guard let self = self, + let view = self.view, + view.isVisibleInDelegate() else { return } + self.halted = false + } + let onHide: () -> Void = { [weak self] in + self?.halted = true + } + + guard visibleBehavior == nil else { + visibleBehavior?.pageShownHandler = onShow + visibleBehavior?.pageHiddenHandler = onHide + return + } + + guard var delegate = delegateObject?.behaviorTemplateDelegate else { return } + let pauseBehavior = PageVisibilityClosureBehavior(with: onShow, onPageHiddenHandler: onHide) + delegate.add(behavior: pauseBehavior) + self.visibleBehavior = pauseBehavior + } + + /// Adds a behavior to pause the video if scrolled off of the page and unpause if necessary if scrolled on. + open func addScrollBehavior(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + + let onScroll = { [weak self] (scrollView: UIScrollView) in + // If visible to not visible, pause video. + // If not visible to visible, unpause if needed, add visible behavior + guard let self = self, + let view = self.view else { return } + self.halted = !view.isVisible(in: scrollView) + } + + guard scrollBehavior == nil else { + scrollBehavior?.pageScrolledHandler = onScroll + return + } + + guard var delegate = delegateObject?.behaviorTemplateDelegate else { return } + let scrollBehavior = PageScrolledClosureBehavior(with: onScroll) + delegate.add(behavior: scrollBehavior) + self.scrollBehavior = scrollBehavior + } + + open func addActiveListener(for view: Video, delegateObject: MVMCoreUIDelegateObject?) { + removeActiveListener() + + resignActiveListener = NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in + self?.halted = true + } + activeListener = NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (notification) in + guard let self = self, + let view = self.view, + view.isVisibleInDelegate() else { return } + self.halted = false + } + } + + private func removeActiveListener() { + if let observer = activeListener { + NotificationCenter.default.removeObserver(observer) + activeListener = nil + } + if let observer = resignActiveListener { + NotificationCenter.default.removeObserver(observer) + resignActiveListener = nil + } + } + + deinit { + removeActiveListener() + } +} diff --git a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift index ff262876..3dfed18d 100644 --- a/MVMCoreUI/Atomic/MoleculeObjectMapping.swift +++ b/MVMCoreUI/Atomic/MoleculeObjectMapping.swift @@ -79,6 +79,7 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: DigitEntryField.self, viewModelClass: DigitEntryFieldModel.self) MoleculeObjectMapping.shared()?.register(viewClass: ItemDropdownEntryField.self, viewModelClass: ItemDropdownEntryFieldModel.self) MoleculeObjectMapping.shared()?.register(viewClass: DateDropdownEntryField.self, viewModelClass: DateDropdownEntryFieldModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: MultiItemDropdownEntryField.self, viewModelClass: MultiItemDropdownEntryFieldModel.self) // MARK:- Selectors MoleculeObjectMapping.shared()?.register(viewClass: RadioButton.self, viewModelClass: RadioButtonModel.self) @@ -106,6 +107,7 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: RadioButtonLabel.self, viewModelClass: RadioButtonLabelModel.self) MoleculeObjectMapping.shared()?.register(viewClass: WebView.self, viewModelClass: WebViewModel.self) MoleculeObjectMapping.shared()?.register(viewClass: LoadingSpinner.self, viewModelClass: LoadingSpinnerModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: Video.self, viewModelClass: VideoModel.self) // MARK:- Horizontal Combination Molecules MoleculeObjectMapping.shared()?.register(viewClass: StringAndMoleculeView.self, viewModelClass: StringAndMoleculeModel.self) @@ -151,6 +153,7 @@ import Foundation MoleculeObjectMapping.shared()?.register(viewClass: Scroller.self, viewModelClass: ScrollerModel.self) MoleculeObjectMapping.shared()?.register(viewClass: ModuleMolecule.self, viewModelClass: ModuleMoleculeModel.self) MoleculeObjectMapping.shared()?.register(viewClass: BGImageMolecule.self, viewModelClass: BGImageMoleculeModel.self) + MoleculeObjectMapping.shared()?.register(viewClass: BGVideoImageMolecule.self, viewModelClass: BGVideoImageMoleculeModel.self) MoleculeObjectMapping.shared()?.register(viewClass: MoleculeSectionHeader.self, viewModelClass: MoleculeSectionHeaderModel.self) MoleculeObjectMapping.shared()?.register(viewClass: MoleculeSectionFooter.self, viewModelClass: MoleculeSectionFooterModel.self) @@ -259,6 +262,7 @@ import Foundation try? ModelRegistry.register(ActionTopAlertModel.self) try? ModelRegistry.register(ActionCollapseNotificationModel.self) try? ModelRegistry.register(ActionOpenPanelModel.self) + try? ModelRegistry.register(ActionTopNotificationModel.self) // MARK:- Behaviors try? ModelRegistry.register(ScreenBrightnessModifierBehavior.self) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinks.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinks.swift index d6175136..93d1526f 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinks.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretAllTextLinks.swift @@ -11,7 +11,7 @@ // MARK: - Outlets //-------------------------------------------------- - public let leftImage = LoadImageView() + public let leftImage = LoadImageView(pinnedEdges: .all) public let eyebrowHeadlineBodyLink = EyebrowHeadlineBodyLink() public let rightLabel = Label(fontStyle: .RegularBodySmall) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift index 4685cafd..b9e4a3e1 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/LeftVariable/ListLeftVariableIconWithRightCaretBodyText.swift @@ -12,7 +12,7 @@ // MARK: - Outlets //-------------------------------------------------- - public let leftImage = LoadImageView() + public let leftImage = LoadImageView(pinnedEdges: .all) public let headlineBody = HeadlineBody() public let rightLabel = Label(fontStyle: .RegularBodySmall) public let rightLabelStackItem: StackItem diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinks.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinks.swift index b05098d4..3b6a5b76 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinks.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/RightVariable/ListRightVariableRightCaretAlltextAndLinks.swift @@ -21,7 +21,8 @@ //-------------------------------------------------- public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - stack = Stack.createStack(with: [(view: eyebrowHeadlineBodyLink, model: StackItemModel(horizontalAlignment: .leading, verticalAlignment: .top)), (view: rightLabel, model: StackItemModel(horizontalAlignment:.fill))], axis: .horizontal) + // Fill for left vertical alignment because bottom constraint was breaking with leading. CXTDT-145456 + stack = Stack.createStack(with: [(view: eyebrowHeadlineBodyLink, model: StackItemModel(horizontalAlignment: .leading, verticalAlignment: .fill)), (view: rightLabel, model: StackItemModel(horizontalAlignment:.fill, verticalAlignment: .leading))], axis: .horizontal) super.init(style: style, reuseIdentifier: reuseIdentifier) } @@ -47,7 +48,7 @@ if let heroCenter = heroCenter, let stackItem = stack.stackItems.last as? StackItem { let convertedPoint = stack.convert(heroCenter, from: self) - stackItem.containerHelper.alignCenterVerticalConstraint?.constant = convertedPoint.y - stack.bounds.midY + stackItem.containerHelper.topConstraint?.constant = max(convertedPoint.y - rightLabel.bounds.midY, 0.0) } return heroCenter @@ -61,7 +62,6 @@ super.set(with: model, delegateObject, additionalData) guard let model = model as? ListRightVariableRightCaretAllTextAndLinksModel else { return } - rightLabel.set(with: model.rightLabel, delegateObject, additionalData) eyebrowHeadlineBodyLink.set(with: model.eyebrowHeadlineBodyLink, delegateObject, additionalData) updateAccessibilityLabel() diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift index 6c8e2c55..ff9a6766 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/Header.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation open class HeaderView: Container { public let line = Line() @@ -15,7 +14,7 @@ open class HeaderView: Container { open var molecule: MoleculeViewProtocol? var headerModel: HeaderModel? { - get { return model as? HeaderModel } + get { model as? HeaderModel } } /// Convenience function to add a molecule to the view. @@ -64,6 +63,6 @@ open class HeaderView: Container { } open override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return PaddingDefaultVerticalSpacing + PaddingDefaultVerticalSpacing + PaddingDefaultVerticalSpacing + PaddingDefaultVerticalSpacing } } diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderModel.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderModel.swift index b8f2144e..7b62cb1f 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderModel.swift @@ -6,7 +6,6 @@ // Copyright © 2019 Suresh, Kamlesh. All rights reserved. // -import Foundation @objcMembers public class MoleculeHeaderModel: HeaderModel, MoleculeModelProtocol, MoleculeContainerModelProtocol { public static var identifier: String = "header" diff --git a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderView.swift b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderView.swift index 2d4f84ff..1bbb78f9 100644 --- a/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderView.swift +++ b/MVMCoreUI/Atomic/Molecules/HeadersAndFooters/MoleculeHeaderView.swift @@ -17,7 +17,7 @@ public class MoleculeHeaderView: MoleculeContainer { var line = Line() var headerModel: MoleculeHeaderModel? { - get { return model as? MoleculeHeaderModel } + get { model as? MoleculeHeaderModel } } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift index 1ee7ee71..4a6e3ca7 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBar.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers open class TabBar: UITabBar, MoleculeViewProtocol, TabBarProtocol, UITabBarDelegate { @@ -15,9 +14,7 @@ import Foundation public let line = Line() required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { - guard let model = model as? TabBarModel else { - fatalError("model is not TabBarModel") - } + guard let model = model as? TabBarModel else { fatalError("model is not TabBarModel") } self.model = model super.init(frame: .zero) @@ -32,7 +29,7 @@ import Foundation fatalError("init(coder:) has not been implemented") } - open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { + open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { guard let model = model as? TabBarModel else { return } self.model = model @@ -57,8 +54,12 @@ import Foundation var tabs: [UITabBarItem] = [] for (index, tab) in model.tabs.enumerated() { let tabBarItem = UITabBarItem(title: tab.title, image: MVMCoreCache.shared()?.getImageFromRegisteredBundles(tab.image), tag: index) - tabBarItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -3) - tabBarItem.setTitleTextAttributes([NSAttributedString.Key.font: MFFonts.mfFontTXRegular(8)], for: .normal) + tabBarItem.accessibilityLabel = tab.accessibilityText + if #available(iOS 13.0, *) { + } else { + tabBarItem.titlePositionAdjustment = UIOffset(horizontal: 0, vertical: -3) + tabBarItem.setTitleTextAttributes([NSAttributedString.Key.font: MFFonts.mfFontTXRegular(8)], for: .normal) + } tabs.append(tabBarItem) } setItems(tabs, animated: false) @@ -100,10 +101,7 @@ import Foundation }) } - public func currentTabIndex() -> Int { - return model.selectedTab - } + public func currentTabIndex() -> Int { model.selectedTab } } -extension UITabBarItem: MFButtonProtocol { -} +extension UITabBarItem: MFButtonProtocol { } diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift index 9de2cd54..fc3e9449 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift @@ -60,17 +60,19 @@ public class TabBarModel: MoleculeModelProtocol { } public class TabBarItemModel: Codable { - var title: String + var title: String? var image: String var action: ActionModelProtocol + var accessibilityText: String? private enum CodingKeys: String, CodingKey { case title case image case action + case accessibilityText } - public init(with title: String, image: String, action: ActionModelProtocol) { + public init(with title: String?, image: String, action: ActionModelProtocol) { self.title = title self.image = image self.action = action @@ -78,15 +80,17 @@ public class TabBarItemModel: Codable { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - title = try typeContainer.decode(String.self, forKey: .title) + title = try typeContainer.decodeIfPresent(String.self, forKey: .title) image = try typeContainer.decode(String.self, forKey: .image) action = try typeContainer.decodeModel(codingKey: .action) + accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(title, forKey: .title) + try container.encodeIfPresent(title, forKey: .title) try container.encode(image, forKey: .image) try container.encodeModel(action, forKey: .action) + try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift index 1b9ce0dc..be67bfca 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItemModel.swift @@ -9,12 +9,12 @@ import Foundation -@objcMembers public class CarouselItemModel: MoleculeCollectionItemModel, CarouselItemModelProtocol { +@objcMembers open class CarouselItemModel: MoleculeCollectionItemModel, CarouselItemModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - public override class var identifier: String { + open override class var identifier: String { return "carouselItem" } @@ -44,7 +44,7 @@ import Foundation try super.init(from: decoder) } - public override func encode(to encoder: Encoder) throws { + open override func encode(to encoder: Encoder) throws { try super.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(peakingUI, forKey: .peakingUI) diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 645d16b1..f9df766f 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -9,7 +9,7 @@ import Foundation /// A model for a collection item that is a container for any molecule. -@objcMembers public class MoleculeCollectionItemModel: MoleculeContainerModel, CollectionItemModelProtocol { +@objcMembers open class MoleculeCollectionItemModel: MoleculeContainerModel, CollectionItemModelProtocol { open override class var identifier: String { return "collectionItem" } diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/ImageBarButtonItem.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/ImageBarButtonItem.swift index 564b9f42..33ed39b8 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/ImageBarButtonItem.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/ImageBarButtonItem.swift @@ -6,17 +6,15 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers open class ImageBarButtonItem: BarButtonItem { - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- public static func create(with image: UIImage?) -> Self { let actionObject = ActionDelegate() - let button = self.init(image: image, style: .plain, target: actionObject, action: #selector(actionObject.callActionBlock(_:))) + let button = self.init(image: image, style: .plain, target: actionObject, action: #selector(actionObject.callActionBlock)) button.actionDelegate = actionObject return button } @@ -29,7 +27,7 @@ import Foundation } /// Creates the item with the passed in action map. - public static func create(with image: UIImage?, actionMap: [AnyHashable : Any], delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Self { + public static func create(with image: UIImage?, actionMap: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Self { let button = create(with: image) button.set(with: actionMap, delegateObject: delegateObject, additionalData: additionalData) return button @@ -42,4 +40,3 @@ import Foundation return button } } - diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift index 989148d1..5175defe 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/LabelBarButtonItem.swift @@ -6,10 +6,8 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers open class LabelBarButtonItem: BarButtonItem { - //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift index 2cfcfbdc..f18c9fa1 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift @@ -5,30 +5,47 @@ // Created by Scott Pfeil on 5/18/20. // -import Foundation public class NavigationImageButtonModel: NavigationButtonModelProtocol, MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var backgroundColor: Color? + public var accessibilityIdentifier: String? public static var identifier: String = "navigationImageButton" - public var image: String public var action: ActionModelProtocol public var accessibilityText: String? + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(with image: String, action: ActionModelProtocol) { self.image = image self.action = action } + //-------------------------------------------------- + // MARK: - Coding Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case image case action + case accessibilityIdentifier case moleculeName case accessibilityText } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) image = try typeContainer.decode(String.self, forKey: .image) action = try typeContainer.decodeModel(codingKey: .action) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) @@ -37,19 +54,26 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(image, forKey: .image) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeModel(action, forKey: .action) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) } + //-------------------------------------------------- + // MARK: - Method + //-------------------------------------------------- + /// Convenience function that creates a BarButtonItem for the model. public func createNavigationItemButton(delegateObject: MVMCoreUIDelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) -> UIBarButtonItem { let uiImage = MVMCoreCache.shared()?.getImageFromRegisteredBundles(image) - let navigationImageButton = ImageBarButtonItem.create(with: uiImage, actionModel: action, delegateObject: delegateObject, additionalData: additionalData) + let buttonItem = ImageBarButtonItem.create(with: uiImage, actionModel: action, delegateObject: delegateObject, additionalData: additionalData) + buttonItem.accessibilityIdentifier = accessibilityIdentifier ?? image if let accessibilityString = accessibilityText { - navigationImageButton.accessibilityLabel = accessibilityString - navigationImageButton.isAccessibilityElement = true + buttonItem.accessibilityLabel = accessibilityString + buttonItem.isAccessibilityElement = true } - return navigationImageButton + + return buttonItem } } diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift index 8d9f85ae..e52ea0d5 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift @@ -6,28 +6,45 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation public class NavigationLabelButtonModel: NavigationButtonModelProtocol, MoleculeModelProtocol { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public var backgroundColor: Color? public static var identifier: String = "navigationLabelButton" - + public var accessibilityIdentifier: String? public var title: String public var action: ActionModelProtocol + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(with title: String, action: ActionModelProtocol) { self.title = title self.action = action } + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName + case accessibilityIdentifier case title case action } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) } @@ -35,10 +52,15 @@ public class NavigationLabelButtonModel: NavigationButtonModelProtocol, Molecule open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(title, forKey: .title) try container.encodeModel(action, forKey: .action) } + //-------------------------------------------------- + // MARK: - Methods + //-------------------------------------------------- + /// Convenience function that creates a BarButtonItem for the model. public func createNavigationItemButton(delegateObject: MVMCoreUIDelegateObject? = nil, additionalData: [AnyHashable: Any]? = nil) -> UIBarButtonItem { return LabelBarButtonItem.create(with: title, actionModel: action, delegateObject: delegateObject, additionalData: additionalData) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 15ef632e..5fab61f9 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -6,12 +6,13 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtocol { - open class var identifier: String { - return "navigationBar" - } + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + open class var identifier: String { "navigationBar" } open var title: String? open var hidden: Bool @@ -28,13 +29,21 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc open var additionalRightButtons: [(NavigationButtonModelProtocol & MoleculeModelProtocol)]? open var titleView: MoleculeModelProtocol? + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init() { hidden = false - backgroundColor = Color(uiColor: .white) - tintColor = Color(uiColor: .black) + backgroundColor = Color(uiColor: .mvmWhite) + tintColor = Color(uiColor: .mvmBlack) line = LineModel(type: .standard) } + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case title @@ -48,13 +57,17 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc case additionalRightButtons case titleView } - + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) title = try typeContainer.decodeIfPresent(String.self, forKey: .title) hidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidden) ?? false - backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) ?? Color(uiColor: .white) - tintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .tintColor) ?? Color(uiColor: .black) + backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) ?? Color(uiColor: .mvmWhite) + tintColor = try typeContainer.decodeIfPresent(Color.self, forKey: .tintColor) ?? Color(uiColor: .mvmBlack) line = try typeContainer.decodeIfPresent(LineModel.self, forKey: .line) ?? LineModel(type: .standard) alwaysShowBackButton = try typeContainer.decodeIfPresent(Bool.self, forKey: .alwaysShowBackButton) backButton = try typeContainer.decodeModelIfPresent(codingKey: .backButton) diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift index 2dbfced5..56b77e3e 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGImageMoleculeModel.swift @@ -27,6 +27,12 @@ open class BGImageMoleculeModel: MoleculeContainerModel { if bottomPadding == nil { bottomPadding = PaddingDefaultVerticalSpacing3 } + if image.contentMode == nil { + image.contentMode = .scaleAspectFill + } + if image.imageFormat == nil { + image.imageFormat = "jpg" + } } private enum CodingKeys: String, CodingKey { diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift new file mode 100644 index 00000000..f57298aa --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMolecule.swift @@ -0,0 +1,98 @@ +// +// BGVideoImageMolecule.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 1/26/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import UIKit + +open class BGVideoImageMolecule: BGImageMolecule { + + public let video = Video() + + /// Used to hide video after loaded. + private var stateKVOToken: NSKeyValueObservation? + private var endObserver: NSObjectProtocol? + + open override func setupView() { + super.setupView() + insertSubview(video, aboveSubview: image) + NSLayoutConstraint.constraintPinSubview(toSuperview: video) + } + + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + super.set(with: model, delegateObject, additionalData) + guard let model = model as? BGVideoImageMoleculeModel else { return } + video.set(with: model.video, delegateObject, additionalData) + video.isHidden = shouldVideoBeHidden() + addStateObserver() + } + + open func shouldVideoBeHidden() -> Bool { + guard let model = model as? BGVideoImageMoleculeModel, + model.video.videoDataManager.videoState != .failed else { + return true + } + guard model.video.videoDataManager.videoState == .loaded, + let player = model.video.videoDataManager.player, + let item = player.currentItem, + item.currentTime() == item.duration else { + return false + } + return true + } + + /// Listens and responds to video loading state changes to add the end observer. + private func addStateObserver() { + removeStateObserver() + + guard stateKVOToken == nil, + let model = model as? BGVideoImageMoleculeModel else { return } + + // To know when the video player item is done loading. + stateKVOToken = + model.video.videoDataManager.observe(\.videoState) { [weak self] (item, change) in + guard let self = self, + let model = self.model as? BGVideoImageMoleculeModel, + item == model.video.videoDataManager else { return } + + switch item.videoState { + case .loaded: + self.addEndObserver() + case .failed: + MVMCoreDispatchUtility.performBlock(onMainThread: { + self.video.isHidden = true + }) + default: + break + } + } + } + + private func removeStateObserver() { + stateKVOToken?.invalidate() + stateKVOToken = nil + } + + private func addEndObserver() { + removeStateObserver() + guard let model = model as? BGVideoImageMoleculeModel, + let item = model.video.videoDataManager.player?.currentItem else { return } + endObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: item, queue: OperationQueue.main) { [weak self] (notification) in + self?.video.isHidden = true + } + } + + private func removeEndObserver() { + guard let endObserver = endObserver else { return } + NotificationCenter.default.removeObserver(endObserver) + self.endObserver = nil + } + + deinit { + removeStateObserver() + removeEndObserver() + } +} diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift new file mode 100644 index 00000000..f2ee20af --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/BGVideoImageMoleculeModel.swift @@ -0,0 +1,38 @@ +// +// BGVideoImageMoleculeModel.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 1/26/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import UIKit + +open class BGVideoImageMoleculeModel: BGImageMoleculeModel { + open override class var identifier: String { + return "bgVideoImageContainer" + } + + public var video: VideoModel + + private enum CodingKeys: String, CodingKey { + case video + } + + public init(_ video: VideoModel, image: ImageViewModel, molecule: MoleculeModelProtocol) { + self.video = video + super.init(image, molecule: molecule) + } + + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + video = try typeContainer.decode(VideoModel.self, forKey:.video) + try super.init(from: decoder) + } + + open override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(video, forKey: .video) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift index e28bf057..e1af54e3 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift @@ -6,11 +6,10 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation extension MVMCoreUITopAlertExpandableView: MoleculeViewProtocol { - public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { defaultSetup() guard let model = model as? CollapsableNotificationModel else { return } backgroundColor = model.backgroundColor?.uiColor ?? .mvmGreen @@ -30,6 +29,10 @@ extension MVMCoreUITopAlertExpandableView: MoleculeViewProtocol { MVMCoreUITopAlertBaseView.addAction(to: button, actionMap: topActionMap, additionalData: nil) shortView?.label?.accessibilityTraits = .button } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } } } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift index 157d6522..c6109372 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift @@ -6,17 +6,24 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation extension MVMCoreUITopAlertMainView: MoleculeViewProtocol { - public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { + + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { defaultSetup() guard let model = model as? NotificationModel else { return } + backgroundColor = model.backgroundColor?.uiColor ?? .mvmGreen var actionMap = model.button?.action.toJSON() + if let title = model.button?.title { actionMap?.updateValue(title, forKey: KeyTitle) } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } + setupCloseButton(model.closeButton != nil, animationDelegate: MVMCoreUITopAlertView.sharedGlobal()?.animationDelegate) setup(withMessage: model.headline.text, subMessage: model.body?.text, color: model.headline.textColor?.uiColor ?? .white, actionMap: actionMap, additionalData: nil) } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift index 70ff00bb..3d02cecd 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift @@ -6,52 +6,72 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation open class NotificationModel: MoleculeModelProtocol { - public class var identifier: String { - return "notification" - } + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + + public class var identifier: String { "notification" } + public var accessibilityIdentifier: String? public var backgroundColor: Color? public var headline: LabelModel public var body: LabelModel? public var button: ButtonModel? public var closeButton: NotificationXButtonModel? + //-------------------------------------------------- + // MARK: - Initializer + //-------------------------------------------------- + public init(with headline: LabelModel) { self.headline = headline } + //-------------------------------------------------- + // MARK: - Default + //-------------------------------------------------- + open func setDefault() { if backgroundColor == nil { - backgroundColor = Color(uiColor: .mvmGreen()) + backgroundColor = Color(uiColor: .mvmGreen) } if headline.textColor == nil { - headline.textColor = Color(uiColor: .white) + headline.textColor = Color(uiColor: .mvmWhite) } if body?.textColor == nil { - body?.textColor = Color(uiColor: .white) + body?.textColor = Color(uiColor: .mvmWhite) } if button?.style == nil { button?.style = .secondary } button?.size = .tiny - button?.enabledTextColor = Color(uiColor: .white) - button?.enabledBorderColor = Color(uiColor: .white) + button?.enabledTextColor = Color(uiColor: .mvmWhite) + button?.enabledBorderColor = Color(uiColor: .mvmWhite) } + //-------------------------------------------------- + // MARK: - Coding Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { case moleculeName case backgroundColor + case accessibilityIdentifier case headline case body case button case closeButton } + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) headline = try typeContainer.decode(LabelModel.self, forKey: .headline) body = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .body) button = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .button) @@ -63,6 +83,7 @@ open class NotificationModel: MoleculeModelProtocol { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) + try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(headline, forKey: .headline) try container.encodeIfPresent(body, forKey: .body) try container.encodeIfPresent(button, forKey: .button) diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift index 65c6e18c..d2696722 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButton.swift @@ -26,6 +26,8 @@ import Foundation adjustsImageWhenHighlighted = false accessibilityLabel = MVMCoreUIUtility.hardcodedString(withKey: "AccCloseButton") setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + heightAnchor.constraint(equalToConstant: 16.0).isActive = true + widthAnchor.constraint(equalToConstant: 16.0).isActive = true } open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift index d111d1c5..b3aed7fc 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBody.swift @@ -19,19 +19,15 @@ open class HeadlineBody: View { // MARK: - Constraints //-------------------------------------------------- - var spaceBetweenLabelsConstant = PaddingOne + var spaceBetweenLabelsConstant = Padding.One var spaceBetweenLabels: NSLayoutConstraint? - var leftConstraintTitle: NSLayoutConstraint? - var rightConstraintTitle: NSLayoutConstraint? - var leftConstraintMessage: NSLayoutConstraint? - var rightConstraintMessage: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- func hasText() -> Bool { - return headlineLabel.hasText || messageLabel.hasText + headlineLabel.hasText || messageLabel.hasText } // MARK: - Styling @@ -54,25 +50,25 @@ open class HeadlineBody: View { } } - func styleLandingPageHeader() { + public func styleLandingPageHeader() { headlineLabel.setFontStyle(.Title2XLarge) messageLabel.setFontStyle(.RegularBodySmall) - spaceBetweenLabelsConstant = PaddingTwo + spaceBetweenLabelsConstant = Padding.Two } - func stylePageHeader() { + public func stylePageHeader() { headlineLabel.setFontStyle(.BoldTitleLarge) messageLabel.setFontStyle(.RegularBodySmall) - spaceBetweenLabelsConstant = PaddingOne + spaceBetweenLabelsConstant = Padding.One } - func styleListItem() { + public func styleListItem() { headlineLabel.setFontStyle(.BoldBodySmall) messageLabel.setFontStyle(.RegularBodySmall) spaceBetweenLabelsConstant = 0 } - func styleListItemDivider() { + public func styleListItemDivider() { headlineLabel.setFontStyle(.BoldTitleMedium) messageLabel.setFontStyle(.RegularBodySmall) spaceBetweenLabelsConstant = 0 @@ -86,48 +82,41 @@ open class HeadlineBody: View { super.setupView() backgroundColor = .clear - clipsToBounds = true + isAccessibilityElement = false + shouldGroupAccessibilityChildren = true + accessibilityElements = [headlineLabel, messageLabel] - let view = MVMCoreUICommonViewsUtility.commonView() - addSubview(view) - NSLayoutConstraint.constraintPinSubview(toSuperview: view) - - view.isAccessibilityElement = false - view.shouldGroupAccessibilityChildren = true - view.accessibilityElements = [headlineLabel, messageLabel] - - view.addSubview(headlineLabel) - view.addSubview(messageLabel) + addSubview(headlineLabel) + addSubview(messageLabel) + headlineLabel.setContentCompressionResistancePriority(.required, for: .vertical) headlineLabel.setContentHuggingPriority(.required, for: .vertical) + messageLabel.setContentCompressionResistancePriority(.required, for: .vertical) messageLabel.setContentHuggingPriority(.required, for: .vertical) - view.setContentHuggingPriority(.required, for: .vertical) - headlineLabel.topAnchor.constraint(equalTo: view.topAnchor).isActive = true + headlineLabel.topAnchor.constraint(equalTo: topAnchor).isActive = true spaceBetweenLabels = messageLabel.topAnchor.constraint(equalTo: headlineLabel.bottomAnchor, constant: spaceBetweenLabelsConstant) spaceBetweenLabels?.isActive = true - leftConstraintTitle = headlineLabel.leftAnchor.constraint(equalTo: view.leftAnchor) - leftConstraintTitle?.isActive = true - - rightConstraintTitle = view.rightAnchor.constraint(equalTo: headlineLabel.rightAnchor) - rightConstraintTitle?.isActive = true - - leftConstraintMessage = messageLabel.leftAnchor.constraint(equalTo: view.leftAnchor) - leftConstraintMessage?.isActive = true - - rightConstraintMessage = view.rightAnchor.constraint(equalTo: messageLabel.rightAnchor) - rightConstraintMessage?.isActive = true - - view.bottomAnchor.constraint(equalTo: messageLabel.bottomAnchor).isActive = true + NSLayoutConstraint.activate([ + headlineLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingAnchor.constraint(equalTo: headlineLabel.trailingAnchor), + messageLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + trailingAnchor.constraint(equalTo: messageLabel.trailingAnchor), + bottomAnchor.constraint(equalTo: messageLabel.bottomAnchor) + ]) } open override func updateView(_ size: CGFloat) { super.updateView(size) + setSpacing() headlineLabel.updateView(size) messageLabel.updateView(size) - setSpacing() + + // Provide the label additional size information to help calculate its intrinsic height. + let padding = MFStyler.defaultHorizontalPadding(forSize: size) * 2 + messageLabel.preferredMaxLayoutWidth = size - padding } //-------------------------------------------------- @@ -146,19 +135,18 @@ open class HeadlineBody: View { // MARK: - MoleculeViewProtocol //-------------------------------------------------- - public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return 58 - } + public override class func estimatedHeight(with model: MoleculeModelProtocol, + _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 58 } public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) - guard let headlineBodyModel = model as? HeadlineBodyModel else { return } + guard let model = model as? HeadlineBodyModel else { return } - style(with: headlineBodyModel.style) + style(with: model.style) - headlineLabel.setOptional(with: headlineBodyModel.headline, delegateObject, additionalData) - messageLabel.setOptional(with: headlineBodyModel.body, delegateObject, additionalData) + headlineLabel.setOptional(with: model.headline, delegateObject, additionalData) + messageLabel.setOptional(with: model.body, delegateObject, additionalData) } open override func reset() { diff --git a/MVMCoreUI/Atomic/Organisms/Stack.swift b/MVMCoreUI/Atomic/Organisms/Stack.swift index d112af5e..29646f2e 100644 --- a/MVMCoreUI/Atomic/Organisms/Stack.swift +++ b/MVMCoreUI/Atomic/Organisms/Stack.swift @@ -16,7 +16,7 @@ open class Stack: Container where T: (StackModelProtocol & MoleculeModelProto open var stackItems: [UIView] = [] open var stackModel: T? { - get { return model as? T } + get { model as? T } } //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift new file mode 100644 index 00000000..a7508e39 --- /dev/null +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/AccessibilityModelProtocol.swift @@ -0,0 +1,23 @@ +// +// AccessibilityModelProtocol.swift +// MVMCoreUI +// +// Created by Kevin Christiano on 2/3/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + + +public protocol AccessibilityModelProtocol { + + var accessibilityIdentifier: String? { get set } +} + +public extension AccessibilityModelProtocol { + + var accessibilityIdentifier: String? { + get { nil } + set { } + } +} diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index dda19b35..4f0cd296 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -7,22 +7,16 @@ public enum MolecularError: Swift.Error { } -public protocol MoleculeModelProtocol: ModelProtocol { +public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol { var moleculeName: String { get } var backgroundColor: Color? { get set } } public extension MoleculeModelProtocol { - var moleculeName: String { - get { return Self.identifier } - } + var moleculeName: String { Self.identifier } - static var categoryName: String { - return "\(MoleculeModelProtocol.self)" - } + static var categoryName: String { "\(MoleculeModelProtocol.self)" } - static var categoryCodingKey: String { - return "moleculeName" - } + static var categoryCodingKey: String { "moleculeName" } } diff --git a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift index ac22cd18..1a82333d 100644 --- a/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeListTemplate.swift @@ -29,14 +29,10 @@ open class ModalMoleculeListTemplate: MoleculeListTemplate { super.handleNewData() closeButton = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: { [weak self] _ in - guard let self = self else { - return - } - guard let model = self.templateModel as? ModalListPageTemplateModel, let actionMap = model.closeAction else { - MVMCoreActionHandler.shared()?.handleAction(with: ActionBackModel().toJSON(), additionalData: nil, delegateObject: self.delegateObjectIVar) - return - } - MVMCoreActionHandler.shared()?.handleAction(with: actionMap.toJSON(), additionalData: nil, delegateObject: self.delegateObjectIVar) + guard let self = self else { return } + let closeAction = (self.templateModel as? ModalListPageTemplateModel)?.closeAction ?? + ActionBackModel() + MVMCoreActionHandler.shared()?.asyncHandleAction(with: closeAction, additionalData: nil, delegateObject: self.delegateObjectIVar) }) } diff --git a/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift index 78a1e157..1db1bb9c 100644 --- a/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalMoleculeStackTemplate.swift @@ -18,15 +18,10 @@ open class ModalMoleculeStackTemplate: MoleculeStackTemplate { override open func handleNewData() { super.handleNewData() _ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: {[weak self] _ in - guard let self = self else { - return - } - - guard let model = self.templateModel as? ModalStackPageTemplateModel, let actionMap = model.closeAction else { - MVMCoreActionHandler.shared()?.handleAction(with: ActionBackModel().toJSON(), additionalData: nil, delegateObject: self.delegateObjectIVar) - return - } - MVMCoreActionHandler.shared()?.handleAction(with: actionMap.toJSON(), additionalData: nil, delegateObject: self.delegateObjectIVar) + guard let self = self else { return } + let closeAction = (self.templateModel as? ModalStackPageTemplateModel)?.closeAction ?? + ActionBackModel() + MVMCoreActionHandler.shared()?.asyncHandleAction(with: closeAction, additionalData: nil, delegateObject: self.delegateObjectIVar) }) } } diff --git a/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift b/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift index 77ee705c..424fc6c5 100644 --- a/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ModalSectionListTemplate.swift @@ -19,12 +19,9 @@ open class ModalSectionListTemplate: SectionListTemplate { super.handleNewData() _ = MVMCoreUICommonViewsUtility.addCloseButton(to: view, action: {[weak self] _ in guard let self = self else { return } - guard let model = self.templateModel as? ModalSectionListTemplateModel, - let actionMap = model.closeAction else { - MVMCoreActionHandler.shared()?.handleAction(with: ActionBackModel().toJSON(), additionalData: nil, delegateObject: self.delegateObjectIVar) - return - } - MVMCoreActionHandler.shared()?.handleAction(with: actionMap.toJSON(), additionalData: nil, delegateObject: self.delegateObjectIVar) + let closeAction = (self.templateModel as? ModalSectionListTemplateModel)?.closeAction ?? + ActionBackModel() + MVMCoreActionHandler.shared()?.asyncHandleAction(with: closeAction, additionalData: nil, delegateObject: self.delegateObjectIVar) }) } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index 4b6b565e..ba057444 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -19,7 +19,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol var observer: NSKeyValueObservation? public var templateModel: ListPageTemplateModel? - + //-------------------------------------------------- // MARK: - Computed Properties //-------------------------------------------------- @@ -41,7 +41,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol //-------------------------------------------------- // MARK: - Methods //-------------------------------------------------- - + open override func parsePageJSON() throws { try parseTemplate(json: loadObject?.pageJSON) try super.parsePageJSON() @@ -54,8 +54,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open override func viewForTop() -> UIView { guard let headerModel = templateModel?.header, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(headerModel, delegateObject: delegateObjectIVar) - else { return super.viewForTop() } + 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 { @@ -67,8 +67,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol override open func viewForBottom() -> UIView { guard let footerModel = templateModel?.footer, - let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar) - else { return super.viewForBottom() } + let molecule = MoleculeObjectMapping.shared()?.createMolecule(footerModel, delegateObject: delegateObjectIVar) + else { return super.viewForBottom() } return molecule } @@ -86,7 +86,7 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol //Handle scroll handleScrollToSpecificRow() } - + //-------------------------------------------------- // MARK: - Handle scroll to spefic row @@ -117,12 +117,12 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { guard let moleculeInfo = getMoleculeInfo(for: indexPath), - let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject) - else { return 0 } + let estimatedHeight = (moleculeInfo.class as? MoleculeViewProtocol.Type)?.estimatedHeight(with: moleculeInfo.molecule, delegateObject() as? MVMCoreUIDelegateObject) + else { return 0 } return estimatedHeight } - + open override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return moleculesInfo?.count ?? 0 } @@ -130,8 +130,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol open override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let moleculeInfo = getMoleculeInfo(for: indexPath), - let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier) - else { return UITableViewCell() } + let cell = tableView.dequeueReusableCell(withIdentifier: moleculeInfo.identifier) + else { return UITableViewCell() } (cell as? MoleculeViewProtocol)?.reset() (cell as? MoleculeListCellProtocol)?.setLines(with: templateModel?.line, delegateObject: delegateObjectIVar, additionalData: nil, indexPath: indexPath) @@ -222,8 +222,8 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol func createMoleculeInfo(with listItem: MoleculeModelProtocol?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? { guard let listItem = listItem, - let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(listItem) - else { return nil } + let moleculeClass = MoleculeObjectMapping.shared()?.getMoleculeClass(listItem) + else { return nil } let moleculeName = moleculeClass.nameForReuse(with: listItem, delegateObject() as? MVMCoreUIDelegateObject) ?? listItem.moleculeName diff --git a/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift b/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift index 50e55a45..81c3b6a8 100644 --- a/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/ThreeLayerFillMiddleTemplate.swift @@ -14,6 +14,14 @@ import Foundation return 0 } + open override func loadView() { + super.loadView() + // The height is used to keep the bottom view at the bottom. + if let contentView = contentView, let scrollView = scrollView { + contentView.heightAnchor.constraint(greaterThanOrEqualTo: scrollView.safeAreaLayoutGuide.heightAnchor).isActive = true + } + } + open override func handleNewData() { super.handleNewData() heightConstraint?.isActive = true diff --git a/MVMCoreUI/BaseClasses/BarButtonItem.swift b/MVMCoreUI/BaseClasses/BarButtonItem.swift index 93eb63a3..d41fd57f 100644 --- a/MVMCoreUI/BaseClasses/BarButtonItem.swift +++ b/MVMCoreUI/BaseClasses/BarButtonItem.swift @@ -8,6 +8,7 @@ public typealias BarButtonAction = (BarButtonItem) -> () + @objc class ActionDelegate: NSObject { var buttonAction: BarButtonAction? @objc func callActionBlock(_ sender: BarButtonItem) { @@ -16,7 +17,6 @@ public typealias BarButtonAction = (BarButtonItem) -> () } @objcMembers open class BarButtonItem: UIBarButtonItem, MFButtonProtocol { - //-------------------------------------------------- // MARK: - Delegate //-------------------------------------------------- @@ -43,4 +43,3 @@ public typealias BarButtonAction = (BarButtonItem) -> () } } } - diff --git a/MVMCoreUI/BaseClasses/Button.swift b/MVMCoreUI/BaseClasses/Button.swift index e06a5cb3..1802e25d 100644 --- a/MVMCoreUI/BaseClasses/Button.swift +++ b/MVMCoreUI/BaseClasses/Button.swift @@ -8,6 +8,7 @@ public typealias ButtonAction = (Button) -> () + @objcMembers open class Button: UIButton, MFButtonProtocol, MoleculeViewProtocol { //-------------------------------------------------- // MARK: - Properties @@ -65,7 +66,7 @@ public typealias ButtonAction = (Button) -> () /// Adds a block to be performed for the given event. open func addActionBlock(event: Event, _ buttonBlock: @escaping ButtonAction) { self.buttonAction = buttonBlock - addTarget(self, action: #selector(callActionBlock(_:)), for: event) + addTarget(self, action: #selector(callActionBlock), for: event) } @objc func callActionBlock(_ sender: Button) { @@ -103,6 +104,10 @@ public typealias ButtonAction = (Button) -> () self.backgroundColor = backgroundColor.uiColor } + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } + if let model = model as? EnableableModelProtocol { isEnabled = model.enabled } @@ -119,25 +124,23 @@ public typealias ButtonAction = (Button) -> () // MARK: Overridables // Base classes need to implement these functions otherwise swift won't respect the subclass functions and use the ones in the protocol extension instead. open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - return model.moleculeName + model.moleculeName } - open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return nil - } + open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { nil } - open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { - return nil - } + open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { nil } //-------------------------------------------------- // MARK: - Accessibility //-------------------------------------------------- open override func accessibilityActivate() -> Bool { + guard isEnabled else { return false } buttonAction?(self) return buttonAction != nil } + } // MARK: - MVMCoreViewProtocol @@ -160,6 +163,6 @@ extension Button: MVMCoreViewProtocol { extension Button: AppleGuidelinesProtocol { override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return Self.acceptablyOutsideBounds(point: point, bounds: bounds) + Self.acceptablyOutsideBounds(point: point, bounds: bounds) } } diff --git a/MVMCoreUI/BaseClasses/Control.swift b/MVMCoreUI/BaseClasses/Control.swift index 57c13e9e..a8fde2e5 100644 --- a/MVMCoreUI/BaseClasses/Control.swift +++ b/MVMCoreUI/BaseClasses/Control.swift @@ -51,9 +51,14 @@ import UIKit // MARK:- MoleculeViewProtocol open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { self.model = model + if let backgroundColor = model.backgroundColor { self.backgroundColor = backgroundColor.uiColor } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } } open func reset() { @@ -63,22 +68,18 @@ import UIKit // MARK: Overridables // Base classes need to implement these functions otherwise swift won't respect the subclass functions and use the ones in the protocol extension instead. open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - return model.moleculeName + model.moleculeName } - open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return nil - } + open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { nil } - open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { - return nil - } + open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { nil } } // MARK: - AppleGuidelinesProtocol extension Control: AppleGuidelinesProtocol { override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return Self.acceptablyOutsideBounds(point: point, bounds: bounds) + Self.acceptablyOutsideBounds(point: point, bounds: bounds) } } diff --git a/MVMCoreUI/BaseClasses/ImageView.swift b/MVMCoreUI/BaseClasses/ImageView.swift index 271ee615..9aa003b8 100644 --- a/MVMCoreUI/BaseClasses/ImageView.swift +++ b/MVMCoreUI/BaseClasses/ImageView.swift @@ -53,33 +53,35 @@ open class ImageView: UIImageView, MoleculeViewProtocol { } public func reset() { - backgroundColor = .clear - } - + backgroundColor = .clear + } + public func setAsMolecule() { } //-------------------------------------------------- // MARK: - ModelMoleculeViewProtocol //-------------------------------------------------- - public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { + public func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + self.model = model + if let backgroundColor = model.backgroundColor { self.backgroundColor = backgroundColor.uiColor } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } } open class func nameForReuse(_ model: MoleculeModelProtocol?, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - return model?.moleculeName + model?.moleculeName } - open class func estimatedHeight(forRow molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return nil - } + open class func estimatedHeight(forRow molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { nil } - open class func requiredModules(_ molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { - return nil - } + open class func requiredModules(_ molecule: MoleculeModelProtocol?, delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { nil } } // MARK:- MVMCoreViewProtocol @@ -94,4 +96,3 @@ extension ImageView: MVMCoreViewProtocol { MVMCoreUIUtility.setMarginsFor(self, leading: 0, top: 0, trailing: 0, bottom: 0) } } - diff --git a/MVMCoreUI/BaseClasses/SectionHeaderFooterView.swift b/MVMCoreUI/BaseClasses/SectionHeaderFooterView.swift index b4b7f8eb..ab85e96f 100644 --- a/MVMCoreUI/BaseClasses/SectionHeaderFooterView.swift +++ b/MVMCoreUI/BaseClasses/SectionHeaderFooterView.swift @@ -6,7 +6,6 @@ // Copyright © 2020 Verizon Wireless. All rights reserved. // -import Foundation @objcMembers open class SectionHeaderFooterView: UITableViewHeaderFooterView, MoleculeViewProtocol { //-------------------------------------------------- @@ -43,10 +42,16 @@ import Foundation //-------------------------------------------------- open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + self.model = model + if let backgroundColor = model.backgroundColor { contentView.backgroundColor = backgroundColor.uiColor } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } } open func reset() { @@ -56,16 +61,12 @@ import Foundation // MARK: Overridables // Base classes need to implement these functions otherwise swift won't respect the subclass functions and use the ones in the protocol extension instead. open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - return model.moleculeName + model.moleculeName } - open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return nil - } + open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { nil } - open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { - return nil - } + open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { nil } } // MARK:- MVMCoreViewProtocol diff --git a/MVMCoreUI/BaseClasses/TableView.swift b/MVMCoreUI/BaseClasses/TableView.swift index 9fb07ac7..bc899084 100644 --- a/MVMCoreUI/BaseClasses/TableView.swift +++ b/MVMCoreUI/BaseClasses/TableView.swift @@ -11,7 +11,7 @@ import Foundation @objcMembers open class TableView: UITableView { /// A block that gets called on tableview frame changes - public var frameChangeAction: (() -> Void)? + public var frameChangeAction: (() -> ())? private var previousFrame = CGRect.zero diff --git a/MVMCoreUI/BaseClasses/TextField.swift b/MVMCoreUI/BaseClasses/TextField.swift index 8795054b..9174426d 100644 --- a/MVMCoreUI/BaseClasses/TextField.swift +++ b/MVMCoreUI/BaseClasses/TextField.swift @@ -93,6 +93,10 @@ extension TextField: MoleculeViewProtocol { if let color = model.backgroundColor?.uiColor { backgroundColor = color } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } } open func reset() { diff --git a/MVMCoreUI/BaseClasses/TextView.swift b/MVMCoreUI/BaseClasses/TextView.swift index 2c609e02..8d09ca35 100644 --- a/MVMCoreUI/BaseClasses/TextView.swift +++ b/MVMCoreUI/BaseClasses/TextView.swift @@ -106,8 +106,8 @@ import UIKit open func reset() { - fontStyle = Styler.Font.RegularBodyLarge - placeholderFontStyle = Styler.Font.RegularMicro + fontStyle = .RegularBodyLarge + placeholderFontStyle = .RegularMicro placeholderTextColor = .mvmCoolGray3 textColor = .mvmBlack isEnabled = true diff --git a/MVMCoreUI/BaseClasses/UICollectionViewLeftAlignedLayout.swift b/MVMCoreUI/BaseClasses/UICollectionViewLeftAlignedLayout.swift index 717bbc55..080b4e88 100644 --- a/MVMCoreUI/BaseClasses/UICollectionViewLeftAlignedLayout.swift +++ b/MVMCoreUI/BaseClasses/UICollectionViewLeftAlignedLayout.swift @@ -9,8 +9,8 @@ import Foundation -class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout { - override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { +public class UICollectionViewLeftAlignedLayout: UICollectionViewFlowLayout { + public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil } var newAttributesForElementsInRect = [UICollectionViewLayoutAttributes]() for attribute in attributes { diff --git a/MVMCoreUI/BaseClasses/View.swift b/MVMCoreUI/BaseClasses/View.swift index cfb6ff1f..deb1bdfa 100644 --- a/MVMCoreUI/BaseClasses/View.swift +++ b/MVMCoreUI/BaseClasses/View.swift @@ -58,6 +58,10 @@ import UIKit if let backgroundColor = model.backgroundColor { self.backgroundColor = backgroundColor.uiColor } + + if let accessibilityIdentifier = model.accessibilityIdentifier { + self.accessibilityIdentifier = accessibilityIdentifier + } } open func reset() { @@ -67,16 +71,12 @@ import UIKit // MARK: Overridables // Base classes need to implement these functions otherwise swift won't respect the subclass functions and use the ones in the protocol extension instead. open class func nameForReuse(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> String? { - return model.moleculeName + model.moleculeName } - open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { - return nil - } + open class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { nil } - open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { - return nil - } + open class func requiredModules(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> [String]? { nil } } // MARK:- MVMCoreViewProtocol diff --git a/MVMCoreUI/BaseControllers/ScrollingViewController.swift b/MVMCoreUI/BaseControllers/ScrollingViewController.swift index fcbcf878..19c9d001 100644 --- a/MVMCoreUI/BaseControllers/ScrollingViewController.swift +++ b/MVMCoreUI/BaseControllers/ScrollingViewController.swift @@ -73,6 +73,12 @@ open class ScrollingViewController: ViewController { scrollView.flashScrollIndicators() } + open func scrollViewDidScroll(_ scrollView: UIScrollView) { + executeBehaviors { (behavior: PageScrolledBehavior) in + behavior.pageScrolled(scrollView: scrollView) + } + } + //-------------------------------------------------- // MARK: - Keyboard Handling //-------------------------------------------------- diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index 33c47344..ecb4089a 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -18,12 +18,8 @@ import UIKit @objc public var loadObject: MVMCoreLoadObject? public var model: MVMControllerModelProtocol? public var pageModel: PageModelProtocol? { - get { - return model - } - set { - model = newValue as? MVMControllerModelProtocol - } + get { model } + set { model = newValue as? MVMControllerModelProtocol } } /// Set if this page is containted in a manager. @@ -31,19 +27,17 @@ import UIKit /// A temporary iVar backer for delegateObject() until we change the protocol public lazy var delegateObjectIVar: MVMCoreUIDelegateObject = { - return MVMCoreUIDelegateObject.create(withDelegateForAll: self) + MVMCoreUIDelegateObject.create(withDelegateForAll: self) }() - public func delegateObject() -> DelegateObject? { - return delegateObjectIVar - } + public func delegateObject() -> DelegateObject? { delegateObjectIVar } public var formValidator: FormValidator? public var needsUpdateUI = false private var observingForResponses = false private var initialLoadFinished = false - private var previousScreenSize = CGSize.zero + public var previousScreenSize = CGSize.zero public var selectedField: UIView? @@ -52,7 +46,7 @@ import UIKit /// Checks if the screen width has changed open func screenSizeChanged() -> Bool { - return !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) + !MVMCoreGetterUtility.cgfequalwiththreshold(previousScreenSize.width, view.bounds.size.width, 0.1) } //-------------------------------------------------- @@ -61,8 +55,8 @@ import UIKit open func observeForResponseJSONUpdates() { guard !observingForResponses, - (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) - else { return } + (pagesToListenFor()?.count ?? 0 > 0 || modulesToListenFor()?.count ?? 0 > 0) + else { return } observingForResponses = true NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) @@ -80,28 +74,28 @@ import UIKit } open func modulesToListenFor() -> [String]? { - return loadObject?.requestParameters?.modules as? [String] + loadObject?.requestParameters?.allModules() } @objc open func responseJSONUpdated(notification: Notification) { // Checks for a page we are listening for. var newData = false if let pagesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyPageMap), - let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in - guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), - let pageType = page.optionalStringForKey(KeyPageType), - pageType == pageTypeListened - else { return false } - - return true - }) { + let pageType = pagesToListenFor()?.first(where: { (pageTypeListened) -> Bool in + guard let page = pagesLoaded.optionalDictionaryForKey(pageTypeListened), + let pageType = page.optionalStringForKey(KeyPageType), + pageType == pageTypeListened + else { return false } + + return true + }) { newData = true loadObject?.pageJSON = pagesLoaded.optionalDictionaryForKey(pageType) } // Checks for modules we are listening for. if let modulesLoaded = notification.userInfo?.optionalDictionaryForKey(KeyModuleMap), - let modulesListened = modulesToListenFor() { + let modulesListened = modulesToListenFor() { for moduleName in modulesListened { if let module = modulesLoaded.optionalDictionaryForKey(moduleName) { newData = true @@ -196,9 +190,9 @@ import UIKit open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer) -> Bool { guard let pageType = loadObject?.pageType, - var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType), - !modulesRequired.isEmpty - else { return true } + var modulesRequired = MVMCoreUIViewControllerMappingObject.shared()?.modulesRequired(forPageType: pageType), + !modulesRequired.isEmpty + else { return true } guard let loadedModules = loadObject?.modulesJSON else { return false } @@ -263,8 +257,8 @@ import UIKit /// Sets the navigation item for this view controller. open func setNavigationItem() { guard let navigationItemModel = getNavigationModel(), - let navigationController = navigationController - else { return } + let navigationController = navigationController + else { return } // Utilize helper function to set the navigation item state. NavigationController.setNavigationItem(navigationController: navigationController, navigationItemModel: navigationItemModel, viewController: self) @@ -273,9 +267,9 @@ import UIKit /// Sets the appearance of the navigation bar based on the model. open func setNavigationBar() { guard let navigationItemModel = getNavigationModel(), - let navigationController = navigationController else { - MVMCoreUISession.sharedGlobal()?.splitViewController?.parent?.setNeedsStatusBarAppearanceUpdate() - return + let navigationController = navigationController else { + MVMCoreUISession.sharedGlobal()?.splitViewController?.parent?.setNeedsStatusBarAppearanceUpdate() + return } // Utilize helper function to set the split view and navigation item state. @@ -354,8 +348,8 @@ import UIKit // First update should be explicit (hence the zero check) if needsUpdateUI || (previousScreenSize != .zero && screenSizeChanged()) { - updateViews() needsUpdateUI = false + updateViews() } previousScreenSize = view.bounds.size; @@ -405,7 +399,7 @@ import UIKit } open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { - return MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait + MVMCoreGetterUtility.isOnIPad() ? UIInterfaceOrientationMask.all : UIInterfaceOrientationMask.portrait } open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { @@ -424,6 +418,7 @@ import UIKit open func viewControllerReady(inManager manager: UIViewController & MVMCoreViewManagerProtocol) { pageShown() } + //-------------------------------------------------- // MARK: - MVMCoreLoadDelegateProtocol //-------------------------------------------------- @@ -435,15 +430,30 @@ import UIKit // Open the support panel if error == nil, - loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true { + loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true { MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true) } } - + /// Override this method to avoid adding form params. open func addFormParams(_ requestParameters: MVMCoreRequestParameters) { formValidator?.addFormParams(requestParameters: requestParameters) } + + public func handleFieldErrors(_ fieldErrors: [Any]?, loadObject: MVMCoreLoadObject) { + + for case let fieldError as [AnyHashable: Any] in fieldErrors ?? [] { + + guard let fieldName = fieldError["fieldName"] as? String, + let userError = fieldError["userMessage"] as? String, + let entryFieldModel = formValidator?.fields[fieldName] as? EntryFieldModel + else { continue } + + entryFieldModel.shouldClearText = fieldError["clearText"] as? Bool ?? true + entryFieldModel.dynamicErrorMessage = userError + } + } + //-------------------------------------------------- // MARK: - MVMCoreActionDelegateProtocol //-------------------------------------------------- @@ -469,9 +479,9 @@ import UIKit open func getModuleWithName(_ moleculeName: String) -> MoleculeModelProtocol? { guard let moduleJSON = loadObject?.modulesJSON?.optionalDictionaryForKey(moleculeName), - let moleculeName = moduleJSON.optionalStringForKey("moleculeName"), - let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) - else { return nil } + let moleculeName = moduleJSON.optionalStringForKey("moleculeName"), + let modelType = ModelRegistry.getType(for: moleculeName, with: MoleculeModelProtocol.self) + else { return nil } do { return try modelType.decode(jsonDict: moduleJSON) as? MoleculeModelProtocol @@ -483,8 +493,8 @@ import UIKit } // Needed otherwise when subclassed, the extension gets called. - open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) {} - open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { return nil } + open func moleculeLayoutUpdated(_ molecule: MoleculeViewProtocol) { } + open func getIndexPath(for molecule: ListItemModelProtocol & MoleculeModelProtocol) -> IndexPath? { nil } open func addMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], indexPath: IndexPath, animation: UITableView.RowAnimation) { } open func removeMolecules(_ molecules: [ListItemModelProtocol & MoleculeModelProtocol], animation: UITableView.RowAnimation) { } @@ -514,13 +524,13 @@ import UIKit } open func showRightPanelForScreenBeforeLaunchApp() -> Bool { - return loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false + loadObject?.pageJSON?.lenientBoolForKey("showRightPanel") ?? false } // TODO: make molecular open func isOverridingRightButton() -> Bool { guard let rightPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("rightPanelButtonLink") - else { return false } + else { return false } MVMCoreActionHandler.shared()?.handleAction(with: rightPanelLink, additionalData: nil, delegateObject: delegateObject()) return true } @@ -528,7 +538,7 @@ import UIKit // TODO: make molecular open func isOverridingLeftButton() -> Bool { guard let leftPanelLink = loadObject?.pageJSON?.optionalDictionaryForKey("leftPanelButtonLink") - else { return false } + else { return false } MVMCoreActionHandler.shared()?.handleAction(with: leftPanelLink, additionalData: nil, delegateObject: delegateObject()) return true } @@ -536,8 +546,8 @@ import UIKit // Eventually will be moved to Model open func bottomProgress() -> Float? { guard let progressString = loadObject?.pageJSON?.optionalStringForKey(KeyProgressPercent), - let progress = Float(progressString) - else { return nil } + let progress = Float(progressString) + else { return nil } return progress / Float(100) } @@ -558,8 +568,8 @@ import UIKit // TODO: Make this into a protocol if UIAccessibility.isVoiceOverRunning { if let toolBar = textField.inputAccessoryView as? UIToolbar, - let _ = toolBar.items?.last, - let pickerView = textField.inputView as? UIPickerView { + let _ = toolBar.items?.last, + let pickerView = textField.inputView as? UIPickerView { view.accessibilityElements = [pickerView, toolBar] } @@ -610,6 +620,6 @@ import UIKit //-------------------------------------------------- func executeBehaviors(_ behaviorBlock:(_ behavior:T)->Void) { - model?.behaviors?.compactMap({ $0 as? T }).forEach { behaviorBlock($0) } + model?.behaviors?.compactMap { $0 as? T }.forEach { behaviorBlock($0) } } } diff --git a/MVMCoreUI/Behaviors/PageBehavior.swift b/MVMCoreUI/Behaviors/PageBehavior.swift index d8fd99a3..aaf915a1 100644 --- a/MVMCoreUI/Behaviors/PageBehavior.swift +++ b/MVMCoreUI/Behaviors/PageBehavior.swift @@ -10,23 +10,22 @@ import Foundation public protocol PageBehaviorProtocol: ModelProtocol { - // The type of rule + /// The type of rule var behaviorName: String { get } - } public extension PageBehaviorProtocol { var behaviorName: String { - get { return Self.identifier } + get { Self.identifier } } static var categoryCodingKey: String { - return "behaviorName" + "behaviorName" } static var categoryName: String { - return "\(PageBehaviorProtocol.self)" + "\(PageBehaviorProtocol.self)" } } @@ -34,11 +33,31 @@ public protocol PageVisibilityBehavior: PageBehaviorProtocol { func onPageShown() func onPageHidden() +} + +public protocol PageScrolledBehavior: PageBehaviorProtocol { + func pageScrolled(scrollView: UIScrollView) } public protocol PageBehaviorsTemplateProtocol { - var behaviors: [PageBehaviorProtocol]? { get } - + var behaviors: [PageBehaviorProtocol]? { get set } + +} + +public extension PageBehaviorsTemplateProtocol { + mutating func add(behavior: PageBehaviorProtocol) { + var newBehaviors = behaviors ?? [] + newBehaviors.append(behavior) + self.behaviors = newBehaviors + } +} + +public extension MVMCoreUIDelegateObject { + weak var behaviorTemplateDelegate: (PageBehaviorsTemplateProtocol & NSObjectProtocol)? { + get { + return (moleculeDelegate as? PageProtocol)?.pageModel as? (PageBehaviorsTemplateProtocol & NSObjectProtocol) + } + } } diff --git a/MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift b/MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift new file mode 100644 index 00000000..c43d4ecc --- /dev/null +++ b/MVMCoreUI/Behaviors/PageScrolledClosureBehavior.swift @@ -0,0 +1,33 @@ +// +// PageScrolledClosureBehavior.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 2/11/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class PageScrolledClosureBehavior: PageScrolledBehavior { + + public static var identifier = "pageScrolledClosureBehavior" + + public var pageScrolledHandler: (_ scrollView: UIScrollView) -> Void + + public init(with onPageScrolledHandler: @escaping (_ scrollView: UIScrollView) -> Void) { + self.pageScrolledHandler = onPageScrolledHandler + } + + // This class is not meant to be decoded and encoded really. + public required init(from decoder: Decoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageScrolledClosureBehavior does not decode.") + } + + public func encode(to encoder: Encoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageScrolledClosureBehavior does not encode.") + } + + public func pageScrolled(scrollView: UIScrollView) { + pageScrolledHandler(scrollView) + } +} diff --git a/MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift b/MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift new file mode 100644 index 00000000..f5ecd82c --- /dev/null +++ b/MVMCoreUI/Behaviors/PageVisibilityClosureBehavior.swift @@ -0,0 +1,40 @@ +// +// PageVisibilityClosureBehavior.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 2/11/21. +// Copyright © 2021 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class PageVisibilityClosureBehavior: PageVisibilityBehavior { + + public static var identifier = "pageVisibilityClosureBehavior" + + public var pageShownHandler: () -> Void + public var pageHiddenHandler: () -> Void + + public init(with onPageShownHandler: @escaping () -> Void, onPageHiddenHandler: @escaping () -> Void) { + self.pageShownHandler = onPageShownHandler + self.pageHiddenHandler = onPageHiddenHandler + } + + // This class is not meant to be decoded and encoded really. + public required init(from decoder: Decoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageVisibilityClosureBehavior does not decode.") + } + + public func encode(to encoder: Encoder) throws { + throw ModelRegistry.Error.decoderOther(message: "PageVisibilityClosureBehavior does not encode.") + } + + //MARK:- PageVisibilityBehavior + public func onPageShown() { + pageShownHandler() + } + + public func onPageHidden() { + pageHiddenHandler() + } +} diff --git a/MVMCoreUI/Categories/UIStackView+Extension.swift b/MVMCoreUI/Categories/UIStackView+Extension.swift index b88dfb51..b6bc1b05 100644 --- a/MVMCoreUI/Categories/UIStackView+Extension.swift +++ b/MVMCoreUI/Categories/UIStackView+Extension.swift @@ -14,6 +14,19 @@ extension UIStackView: MVMCoreViewProtocol { (view as? MVMCoreViewProtocol)?.updateView(size) } } + + /// A convenience function for updating molecules. If model is nil, view is hidden. + open func updateContainedMolecules(with models: [MoleculeModelProtocol?], _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + for (index, item) in arrangedSubviews.enumerated() { + if let model = models[index] { + (item as? MoleculeViewProtocol)?.set(with: model, delegateObject, additionalData) + item.isHidden = false + } else { + item.isHidden = true + } + } + layoutIfNeeded() + } } extension UIStackView: MoleculeViewProtocol { @@ -23,3 +36,4 @@ extension UIStackView: MoleculeViewProtocol { } } } + diff --git a/MVMCoreUI/Containers/Views/Container.swift b/MVMCoreUI/Containers/Views/Container.swift index 7ad02f9d..159535d7 100644 --- a/MVMCoreUI/Containers/Views/Container.swift +++ b/MVMCoreUI/Containers/Views/Container.swift @@ -15,9 +15,9 @@ open class Container: View, ContainerProtocol { //-------------------------------------------------- public var view: UIView? - let containerHelper = ContainerHelper() + public let containerHelper = ContainerHelper() - var containerModel: ContainerModelProtocol? { + public var containerModel: ContainerModelProtocol? { get { return model as? ContainerModelProtocol } } diff --git a/MVMCoreUI/Containers/Views/ContainerHelper.swift b/MVMCoreUI/Containers/Views/ContainerHelper.swift index f60d1ee6..b60bcc1e 100644 --- a/MVMCoreUI/Containers/Views/ContainerHelper.swift +++ b/MVMCoreUI/Containers/Views/ContainerHelper.swift @@ -15,23 +15,23 @@ open class ContainerHelper: NSObject { // MARK: - Constraints //-------------------------------------------------- - var leftConstraint: NSLayoutConstraint? - var topConstraint: NSLayoutConstraint? - var bottomConstraint: NSLayoutConstraint? - var rightConstraint: NSLayoutConstraint? + open var leftConstraint: NSLayoutConstraint? + open var topConstraint: NSLayoutConstraint? + open var bottomConstraint: NSLayoutConstraint? + open var rightConstraint: NSLayoutConstraint? - var alignCenterHorizontalConstraint: NSLayoutConstraint? - var alignCenterLeftConstraint: NSLayoutConstraint? - var alignCenterRightConstraint: NSLayoutConstraint? + open var alignCenterHorizontalConstraint: NSLayoutConstraint? + open var alignCenterLeftConstraint: NSLayoutConstraint? + open var alignCenterRightConstraint: NSLayoutConstraint? - var alignCenterVerticalConstraint: NSLayoutConstraint? - var alignCenterTopConstraint: NSLayoutConstraint? - var alignCenterBottomConstraint: NSLayoutConstraint? + open var alignCenterVerticalConstraint: NSLayoutConstraint? + open var alignCenterTopConstraint: NSLayoutConstraint? + open var alignCenterBottomConstraint: NSLayoutConstraint? - var leftLowConstraint: NSLayoutConstraint? - var topLowConstraint: NSLayoutConstraint? - var bottomLowConstraint: NSLayoutConstraint? - var rightLowConstraint: NSLayoutConstraint? + open var leftLowConstraint: NSLayoutConstraint? + open var topLowConstraint: NSLayoutConstraint? + open var bottomLowConstraint: NSLayoutConstraint? + open var rightLowConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Methods diff --git a/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift b/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift index b9d2defe..afdde887 100644 --- a/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/FormFieldProtocol.swift @@ -7,10 +7,9 @@ // // Form fields are items can be interacted with. They have value, and may need to be validated. -import Foundation - public protocol FormFieldProtocol: FormItemProtocol { + /// How the validator identifies the field when validating rules. var fieldKey: String? { get set } @@ -22,7 +21,6 @@ public protocol FormFieldProtocol: FormItemProtocol { } extension FormFieldProtocol { - var baseValue: AnyHashable? { - return nil - } + + var baseValue: AnyHashable? { nil } } diff --git a/MVMCoreUI/FormUIHelpers/FormValidator.swift b/MVMCoreUI/FormUIHelpers/FormValidator.swift index e2e725c1..a0785c41 100644 --- a/MVMCoreUI/FormUIHelpers/FormValidator.swift +++ b/MVMCoreUI/FormUIHelpers/FormValidator.swift @@ -75,11 +75,12 @@ import MVMCore /// Convenience function. Gets the form validator from the holder and asks it to validate. public static func validate(delegate: FormHolderProtocol?) -> Bool? { - return delegate?.formValidator?.validate() + delegate?.formValidator?.validate() } /// Validates all rule groups. Returns if valid public func validate() -> Bool { + var valid = true guard let formRules = formRules else { return valid } @@ -109,9 +110,10 @@ import MVMCore } } -// mark Form params +// MARK: Form params // TODO: Temporary hacks, rewrite architecture to support this. @objc public extension FormValidator { + @objc func addFormParams(requestParameters: MVMCoreRequestParameters) { let groupName = getGroupName(forPageType: requestParameters.pageType) ?? FormValidator.defaultGroupName let formParams = self.getFormParams(forGroup: groupName) @@ -119,6 +121,7 @@ import MVMCore } @objc func getFormParams( forGroup groupName: String) -> [String: Any] { + var extraParam: [String: Any] = [:] for (fieldKey, field) in fields { if let formFieldValue = field.formFieldValue(), @@ -126,6 +129,7 @@ import MVMCore extraParam[fieldKey] = formFieldValue } } + return extraParam } } @@ -145,4 +149,3 @@ public extension FormValidator { return nil } } - diff --git a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift index 7392ea8f..5a8b47a8 100644 --- a/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift +++ b/MVMCoreUI/FormUIHelpers/Rules/Rules/RulesProtocol.swift @@ -31,17 +31,11 @@ public protocol RulesProtocol: ModelProtocol { public extension RulesProtocol { - var type: String { - get { return Self.identifier } - } + var type: String { Self.identifier } - static var categoryCodingKey: String { - return "type" - } + static var categoryCodingKey: String { "type" } - static var categoryName: String { - return "\(RulesProtocol.self)" - } + static var categoryName: String { "\(RulesProtocol.self)" } // Individual rule can override the function to validate based on the rule type. func validate(_ fieldMolecules: [String: FormFieldProtocol]) -> Bool { diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index 3a4d2cbc..90532f98 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -13,6 +13,7 @@ import UIKit public var globalTopAlertDelegate: MVMCoreGlobalTopAlertDelegateProtocol? open override func defaultInitialSetup() { + loadHandler = MVMCoreLoadHandler() cache = MVMCoreCache() sessionHandler = MVMCoreSessionTimeHandler() actionHandler = MVMCoreActionHandler() @@ -21,5 +22,6 @@ import UIKit loggingDelegate = MVMCoreUILoggingHandler() moleculeMap = MoleculeObjectMapping() MoleculeObjectMapping.registerObjects() + clientParameterRegistry = ClientParameterRegistry() } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.h b/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.h index 3835207b..c1dee62a 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.h +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.h @@ -24,16 +24,8 @@ NS_ASSUME_NONNULL_BEGIN // Collapses the current top notification - (void)collapseNotificationAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject; -#pragma mark - Deprecated - -// Shows a popup -- (void)popupAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate __deprecated; - -// Shows a top alert -- (void)topAlertAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate __deprecated; - -// Collapses the current top notification -- (void)collapseNotificationAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate __deprecated; +// Shows a topnotification new molecular +- (void)topNotificationAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject; @end diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.m b/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.m index 14888ed1..e2adbd74 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.m @@ -31,6 +31,9 @@ } else if ([actionType isEqualToString:KeyActionTypeAlert]) { [self showAlert:actionInformation additionalData:additionalData delegateObject:delegateObject]; return YES; + } else if ([actionType isEqualToString:KeyActionTypeTopNotification]) { + [self topNotificationAction:actionInformation additionalData:additionalData delegateObject:delegateObject]; + return YES; } return NO; } @@ -102,6 +105,11 @@ } } +- (void)topNotificationAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject { + //Handle molecular topnotification + [[MVMCoreUITopAlertView sharedGlobal] showTopAlertWith:[actionInformation dict:@"topNotification"] ?: @{}]; +} + - (void)defaultHandleActionError:(nonnull MVMCoreErrorObject *)error additionalData:(nullable NSDictionary *)additionalData { [super defaultHandleActionError:error additionalData:additionalData]; if (!error.silentError) { @@ -114,62 +122,4 @@ } } -#pragma mark - Deprecated - -- (BOOL)handleOtherActions:(nullable NSString *)actionType actionInformation:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate { - if ([actionType isEqualToString:KeyActionTypePopup]) { - [self popupAction:actionInformation additionalData:additionalData delegate:delegate]; - return YES; - } else if ([actionType isEqualToString:KeyActionTypeTopAlert]) { - [self topAlertAction:actionInformation additionalData:additionalData delegate:delegate]; - return YES; - } else if ([actionType isEqualToString:KeyActionTypeCollapseNotification]) { - [self collapseNotificationAction:actionInformation additionalData:additionalData delegate:delegate]; - return YES; - } - return NO; -} - -- (void)popupAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate { - // Perform a popup. - NSString *pageTypeForPopup = [actionInformation stringForKey:KeyPageType]; - [[MVMCoreCache sharedCache] fetchJSONForPageType:pageTypeForPopup queue:nil waitUntilFinished:YES completionHandler:^(NSDictionary * _Nullable jsonDictionary) { - - MVMCoreErrorObject *error = nil; - MVMCoreAlertObject *alertObject = [MVMCoreAlertObject alertObjectWithPage:jsonDictionary isGreedy:NO additionalData:additionalData delegate:delegate error:&error]; - if ([delegate respondsToSelector:@selector(willShowPopupWithAlertObject:alertJson:)]) { - [((id )delegate) willShowPopupWithAlertObject:alertObject alertJson:jsonDictionary]; - } - - if (alertObject) { - [[MVMCoreAlertHandler sharedAlertHandler] showAlertWithAlertObject:alertObject]; - } else { - [self handleActionError:error actionInformation:actionInformation additionalData:additionalData delegate:delegate]; - } - }]; -} - -- (void)topAlertAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate { - // Perform a top alert. - NSString *pageTypeForTopAlert = [actionInformation stringForKey:KeyPageType]; - [[MVMCoreCache sharedCache] fetchJSONForPageType:pageTypeForTopAlert queue:nil waitUntilFinished:YES completionHandler:^(NSDictionary * _Nullable jsonDictionary) { - - NSDictionary *responseInfo = [jsonDictionary dict:KeyResponseInfo]; - if (responseInfo) { - MVMCoreAlertObject *alertObject = [MVMCoreAlertObject alertObjectForPageType:pageTypeForTopAlert responseInfo:responseInfo additionalData:additionalData actionDelegate:delegate]; - if ([delegate respondsToSelector:@selector(willShowTopAlertWithAlertObject:alertJson:)]) { - alertObject = [((id )delegate) willShowTopAlertWithAlertObject:alertObject alertJson:jsonDictionary]; - } - [alertObject showAlert]; - } - }]; -} - -- (void)collapseNotificationAction:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData delegate:(nullable NSObject *)delegate { - // Collapse the current notification. - if ([[CoreUIObject sharedInstance].globalTopAlertDelegate respondsToSelector:@selector(getTopAlertView)]) { - [[[CoreUIObject sharedInstance].globalTopAlertDelegate getTopAlertView] collapseNotification]; - } -} - @end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m index 11d10ff0..892a6e94 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m @@ -77,6 +77,8 @@ [[MVMCoreUISession sharedGlobal].topAlertView hideAlertView:YES completionHandler:nil]; } } centeredVertically:YES]; + [closeButton.heightAnchor constraintEqualToConstant:16.0].active = YES; + [closeButton.widthAnchor constraintEqualToConstant:16.0].active = YES; [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:closeButton]; return closeButton; } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m index 55468c95..46ecd793 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m @@ -211,7 +211,7 @@ if (topMessage && (!self.onlyShowTopMessageWhenCollapsed || !self.expanded)) { self.shortViewHeight.active = NO; - } else if (!topMessage && self.onlyShowTopMessageWhenCollapsed && self.expanded) { + } else if (!topMessage || (self.onlyShowTopMessageWhenCollapsed && self.expanded)) { self.shortViewHeight.active = YES; } }]; diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift index f179fb11..107a3596 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift @@ -61,6 +61,12 @@ public extension MVMCoreUITopAlertView { MVMCoreAlertHandler.shared()?.add(operation) } + /// Shows the top alert with the json. + @objc func showTopAlert(with json: [AnyHashable: Any]) { + guard let model = decodeTopNotification(with: json, delegateObject: getDelegateObject()) else { return } + showTopAlert(with: model) + } + /// Checks for existing top alert object of same type and updates it. Only happens for molecular top alerts. Returns true if we updated. private func checkAndUpdateExisting(with topAlertObject: MVMCoreTopAlertObject) -> Bool { guard let queue = MVMCoreAlertHandler.shared()?.topAlertQueue.operations else { return false } diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m index 4ac5aaa0..ad0a833f 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m @@ -113,8 +113,8 @@ NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; - (void)pinATopViewController:(UIViewController *)viewController { self.statusBarHeightConstraint.active = NO; - id topGuide = viewController.topLayoutGuide; - self.statusBarBottomConstraint = [NSLayoutConstraint constraintWithItem:self.statusBarView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:topGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; + id topGuide = viewController.view.safeAreaLayoutGuide; + self.statusBarBottomConstraint = [NSLayoutConstraint constraintWithItem:self.statusBarView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:topGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:0]; self.statusBarBottomConstraint.active = YES; } diff --git a/MVMCoreUI/Utility/MVMCoreUIConstants.h b/MVMCoreUI/Utility/MVMCoreUIConstants.h index b96d50ed..ab77986b 100644 --- a/MVMCoreUI/Utility/MVMCoreUIConstants.h +++ b/MVMCoreUI/Utility/MVMCoreUIConstants.h @@ -48,6 +48,7 @@ extern NSString * const KeyActionTypePopup; extern NSString * const KeyActionTypeAlert; extern NSString * const KeyActionTypeTopAlert; extern NSString * const KeyActionTypeCollapseNotification; +extern NSString * const KeyActionTypeTopNotification; /// Key for molecular top notification architecture. extern NSString * const KeyTopAlert; diff --git a/MVMCoreUI/Utility/MVMCoreUIConstants.m b/MVMCoreUI/Utility/MVMCoreUIConstants.m index 092851cd..2483826f 100644 --- a/MVMCoreUI/Utility/MVMCoreUIConstants.m +++ b/MVMCoreUI/Utility/MVMCoreUIConstants.m @@ -47,6 +47,7 @@ NSString * const KeyActionTypeAlert = @"alert"; NSString * const KeyActionTypeTopAlert = @"topAlert"; NSString * const KeyActionTypeCollapseNotification = @"collapseNotification"; +NSString * const KeyActionTypeTopNotification = @"topNotification"; NSString * const KeyTopAlert = @"TopNotification"; #pragma mark - Values diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.h b/MVMCoreUI/Utility/MVMCoreUIUtility.h index 4bf6e8d5..74b19d0e 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.h +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.h @@ -37,6 +37,14 @@ NS_ASSUME_NONNULL_BEGIN /// Checks if the view or any descendents of the view is currently focused for voice over. + (BOOL)viewContainsAccessiblityFocus:(nonnull UIView *)view; ++ (BOOL)isView:(nonnull UIView *)view visibleIn:(nonnull UIView *)rootView; + ++ (BOOL)isViewVisibleInParent:(nonnull UIView *)view; + ++ (BOOL)doesView:(nonnull UIView *)overlappingView cover:(nonnull UIView *)view; + ++ (BOOL)isViewTransparent:(nonnull UIView *)view; + #pragma mark - Setters + (void)setMarginsForView:(nullable UIView *)view leading:(CGFloat)leading top:(CGFloat)top trailing:(CGFloat)trailing bottom:(CGFloat)bottom; diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.m b/MVMCoreUI/Utility/MVMCoreUIUtility.m index fed0bb30..fad2378c 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.m +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.m @@ -96,6 +96,51 @@ return containsFocus; } ++ (BOOL)isView:(nonnull UIView *)view visibleIn:(nonnull UIView *)rootView { + return [view isDescendantOfView:rootView] && [self isViewVisibleInParent:view]; +} + +// Climb up the tree. Break early. ++ (BOOL)isViewVisibleInParent:(nonnull UIView *)view { + UIView *superview = view.superview; + UIView *ancestor = view; + // Begin climbing its ancestor views, checking if it remains visible in *each* of their bounds and if the children at views closer to the front block the visibility of this view. + while (superview) { + if (superview.clipsToBounds && !CGRectIntersectsRect(superview.bounds, [view convertRect:view.bounds toView:superview])) { + return false; + } else { + // Check the superview's children up to the common ancestor. (Reworking the ancestor would put us in an infinite loop and we want to ignore the children with a lower z-order.) + for (UIView *subview in [superview.subviews reverseObjectEnumerator]) { + if (subview == ancestor) { + break; + } else if ([self doesView:subview cover:view]) { + return false; + } + } + } + ancestor = superview; + superview = superview.superview; + } + return true; +} + +// Climb down the tree. ++ (BOOL)doesView:(nonnull UIView *)overlappingView cover:(nonnull UIView *)view { + if (![self isViewTransparent:overlappingView] && CGRectContainsRect(overlappingView.bounds, [view convertRect:view.bounds toView:overlappingView])) { + return true; + } + for (UIView *subview in overlappingView.subviews) { + if ([self doesView:subview cover:view]) { + return true; + } + } + return false; +} + ++ (BOOL)isViewTransparent:(nonnull UIView *)view { + return view.alpha < 1 || view.backgroundColor == nil || CGColorGetAlpha(view.backgroundColor.CGColor) < 1; +} + #pragma mark - Setters + (void)setMarginsForView:(nullable UIView *)view leading:(CGFloat)leading top:(CGFloat)top trailing:(CGFloat)trailing bottom:(CGFloat)bottom {