diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index beb77bd9..46156d9d 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -129,6 +129,8 @@ 187FEB2A2844D2A600BF29C2 /* VDSFormControlsTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */; }; 1D6D258826899B0C00DEBB08 /* ImageButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6D258626899B0B00DEBB08 /* ImageButtonModel.swift */; }; 1D6D258926899B0C00DEBB08 /* ImageButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D6D258726899B0B00DEBB08 /* ImageButton.swift */; }; + 22B678F929E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B678F829E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift */; }; + 22B678FD29E82B0300CF4196 /* ListNotificationAuthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B678FC29E82B0300CF4196 /* ListNotificationAuthModel.swift */; }; 27559EFC27D691D3000836C1 /* ViewMaskingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */; }; 27577DCD286CA959001EC47E /* MoleculeMaskingProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */; }; 279B1569242BBC2F00921D6C /* ActionModelAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 279B1568242BBC2F00921D6C /* ActionModelAdapter.swift */; }; @@ -202,7 +204,6 @@ 9458C3172406C8FD00930963 /* UIFont+FontWrapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 9458C3152406C8FD00930963 /* UIFont+FontWrapping.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9458C3182406C8FD00930963 /* UIFont+FontWrapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 9458C3162406C8FD00930963 /* UIFont+FontWrapping.m */; }; 948DB67E2326DCD90011F916 /* MultiProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 948DB67D2326DCD90011F916 /* MultiProgress.swift */; }; - 94C0150A24215643005811A9 /* ActionTopAlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C0150924215643005811A9 /* ActionTopAlertModel.swift */; }; 94C0150C2421564A005811A9 /* ActionCollapseNotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C0150B2421564A005811A9 /* ActionCollapseNotificationModel.swift */; }; 94C2D9842386F3F80006CF46 /* LabelAttributeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C2D9832386F3F80006CF46 /* LabelAttributeModel.swift */; }; 94C2D9A123872BCC0006CF46 /* LabelAttributeUnderlineModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C2D9A023872BCC0006CF46 /* LabelAttributeUnderlineModel.swift */; }; @@ -278,15 +279,20 @@ AAE96FA225341F6A0037A989 /* ListStoreLocatorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE96FA125341F6A0037A989 /* ListStoreLocatorModel.swift */; }; AAE96FA525341F7D0037A989 /* ListStoreLocator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE96FA425341F7D0037A989 /* ListStoreLocator.swift */; }; AF1C33652883B5A4006B1001 /* ActionTopNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C33642883B5A4006B1001 /* ActionTopNotificationHandler.swift */; }; - AF1C33672883B712006B1001 /* ActionPopupHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C33662883B712006B1001 /* ActionPopupHandler.swift */; }; AF1C336928859778006B1001 /* ActionAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C336828859778006B1001 /* ActionAlertHandler.swift */; }; - AF1C336B28859C73006B1001 /* ActionTopAlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C336A28859C73006B1001 /* ActionTopAlertHandler.swift */; }; AF1C336D28859EE1006B1001 /* ActionOpenPanelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C336C28859EE1006B1001 /* ActionOpenPanelHandler.swift */; }; AF1C336F2885A16A006B1001 /* ActionCollapseNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C336E2885A16A006B1001 /* ActionCollapseNotificationHandler.swift */; }; AF1C33712885AE76006B1001 /* MVMCoreUIActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C33702885AE76006B1001 /* MVMCoreUIActionHandler.swift */; }; AF1C33732885D481006B1001 /* MVMCoreUIActionOpenPageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1C33722885D481006B1001 /* MVMCoreUIActionOpenPageHandler.swift */; }; AF60A7F62892D2E300919EEB /* ActionDismissNotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF60A7F52892D2E300919EEB /* ActionDismissNotificationModel.swift */; }; AF60A7F82892D34D00919EEB /* ActionDismissNotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF60A7F72892D34D00919EEB /* ActionDismissNotificationHandler.swift */; }; + AF766D262A3CD4C600749099 /* UIAccessibilityTraits+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF766D252A3CD4C600749099 /* UIAccessibilityTraits+Codable.swift */; }; + AF7E509829E477C1009DC2AD /* AlertHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7E509629E477C0009DC2AD /* AlertHandler.swift */; }; + AF7E509929E477C1009DC2AD /* AlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF7E509729E477C0009DC2AD /* AlertController.swift */; }; + AFA4932029E5CA73001A9663 /* AlertOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4931F29E5CA73001A9663 /* AlertOperation.swift */; }; + AFA4932229E5EF2E001A9663 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */; }; + AFA4933F29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */; }; + AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */; }; AFE4A1D127DFB5EE00C458D0 /* VDSColorTokens.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */; }; AFE4A1D627DFBB6F00C458D0 /* UINavigationController+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */; }; B4CC8FBD29DF34680005D28B /* Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4CC8FBC29DF34680005D28B /* Badge.swift */; }; @@ -346,8 +352,8 @@ D2092357244FA1EF0044AD09 /* ThreeLayerModelBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2092356244FA1EF0044AD09 /* ThreeLayerModelBase.swift */; }; D20923592450ECE00044AD09 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20923582450ECE00044AD09 /* TableView.swift */; }; D20A9A5E2243D3E300ADE781 /* TwoButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */; }; - D20C7009250BF99B0095B21C /* TopNotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20C7008250BF99B0095B21C /* TopNotificationModel.swift */; }; - D20C700B250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20C700A250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift */; }; + D20C7009250BF99B0095B21C /* NotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20C7008250BF99B0095B21C /* NotificationModel.swift */; }; + D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20C700A250BFDE40095B21C /* NotificationContainerView.swift */; }; D20F3B44252E00E4004B3F56 /* PageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20F3B43252E00E4004B3F56 /* PageProtocol.swift */; }; D20FB165241A5D75004AFC3A /* NavigationItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20FB164241A5D75004AFC3A /* NavigationItemModel.swift */; }; D213347723843825008E41B3 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = D213347623843825008E41B3 /* Line.swift */; }; @@ -366,7 +372,7 @@ D224799B231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */; }; D22D8393241C27B100D3DF69 /* BaseTemplateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22D8392241C27B100D3DF69 /* BaseTemplateModel.swift */; }; D22D8395241FB41200D3DF69 /* UIStackView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */; }; - D23118B325124E18001C8440 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23118B225124E18001C8440 /* Notification.swift */; }; + D23118B325124E18001C8440 /* NotificationMoleculeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D23118B225124E18001C8440 /* NotificationMoleculeView.swift */; }; D2351C7A24A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */; }; D2351C7C24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */; }; D236E5B4241FEB1000C38625 /* ListTwoColumnPriceDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */; }; @@ -457,16 +463,6 @@ D29DF11821E6805F003B2FB9 /* NSLayoutConstraint+MFConvenience.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF11421E6805F003B2FB9 /* NSLayoutConstraint+MFConvenience.m */; }; D29DF11C21E684A9003B2FB9 /* MVMCoreUISplitViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF11A21E684A9003B2FB9 /* MVMCoreUISplitViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; D29DF11D21E684A9003B2FB9 /* MVMCoreUISplitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF11B21E684A9003B2FB9 /* MVMCoreUISplitViewController.m */; }; - D29DF12921E6851E003B2FB9 /* MVMCoreUITopAlertMainView.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF11F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D29DF12A21E6851E003B2FB9 /* MVMCoreUITopAlertView.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF12021E6851E003B2FB9 /* MVMCoreUITopAlertView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D29DF12B21E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF12121E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m */; }; - D29DF12C21E6851E003B2FB9 /* MVMCoreUITopAlertShortView.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF12221E6851E003B2FB9 /* MVMCoreUITopAlertShortView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D29DF12D21E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF12321E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D29DF12E21E6851E003B2FB9 /* MVMCoreUITopAlertView.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF12421E6851E003B2FB9 /* MVMCoreUITopAlertView.m */; }; - D29DF12F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF12521E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m */; }; - D29DF13021E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF12621E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m */; }; - D29DF13121E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF12721E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */ = {isa = PBXBuildFile; fileRef = D29DF12821E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m */; }; D29DF15421E69760003B2FB9 /* MVMCoreUIPanelButtonProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF15321E69760003B2FB9 /* MVMCoreUIPanelButtonProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; D29DF17521E69E1F003B2FB9 /* ButtonDelegateProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF16B21E69E1F003B2FB9 /* ButtonDelegateProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; D29DF25921E6A22D003B2FB9 /* MFButtonProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D29DF25821E6A22D003B2FB9 /* MFButtonProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -525,10 +521,8 @@ D2C521A923EDE79E00CA2634 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C521A823EDE79E00CA2634 /* ViewController.swift */; }; D2C78CD224228BBD00B69FDE /* ActionOpenPanelModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C78CD124228BBD00B69FDE /* ActionOpenPanelModel.swift */; }; D2CAC7CB251104E100C75681 /* NotificationXButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */; }; - D2CAC7CD251104FE00C75681 /* NotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CAC7CC251104FE00C75681 /* NotificationModel.swift */; }; + D2CAC7CD251104FE00C75681 /* NotificationMoleculeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CAC7CC251104FE00C75681 /* NotificationMoleculeModel.swift */; }; D2CAC7CF2511052300C75681 /* CollapsableNotificationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */; }; - D2CAC7D12511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CAC7D02511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift */; }; - D2CAC7D3251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CAC7D2251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift */; }; D2D2FCF0252B72AF0033EAAA /* MoleculeSectionFooterModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D2FCEF252B72AF0033EAAA /* MoleculeSectionFooterModel.swift */; }; D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D2FCF2252B72CF0033EAAA /* MoleculeSectionFooter.swift */; }; D2D3957A252FDBB300047B11 /* ModalSectionListTemplate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D39579252FDBB300047B11 /* ModalSectionListTemplate.swift */; }; @@ -558,27 +552,9 @@ D2EC7BDD2527B83700F540AF /* SectionHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2EC7BDC2527B83700F540AF /* SectionHeaderFooterView.swift */; }; D2ED27EB254B0CE700A1C293 /* UIAlertActionStyle+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27E6254B0CE600A1C293 /* UIAlertActionStyle+Codable.swift */; }; D2ED27EC254B0CE700A1C293 /* UIAlertControllerStyle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27E7254B0CE600A1C293 /* UIAlertControllerStyle+Extension.swift */; }; - D2ED27ED254B0CE700A1C293 /* ActionPopupModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27E8254B0CE600A1C293 /* ActionPopupModel.swift */; }; D2ED27EE254B0CE700A1C293 /* ActionAlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27E9254B0CE600A1C293 /* ActionAlertModel.swift */; }; D2ED27EF254B0CE700A1C293 /* AlertModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27EA254B0CE700A1C293 /* AlertModel.swift */; }; - D2ED27FB254B0E0300A1C293 /* MVMCoreAlertDelegateProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED27F2254B0E0200A1C293 /* MVMCoreAlertDelegateProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED27FC254B0E0300A1C293 /* MVMCoreAlertObject+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27F3254B0E0200A1C293 /* MVMCoreAlertObject+Swift.swift */; }; - D2ED27FD254B0E0300A1C293 /* MVMCoreAlertOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27F4254B0E0200A1C293 /* MVMCoreAlertOperation.m */; }; - D2ED27FE254B0E0300A1C293 /* MVMCoreAlertObject.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED27F5254B0E0200A1C293 /* MVMCoreAlertObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED27FF254B0E0300A1C293 /* MVMCoreAlertHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED27F6254B0E0200A1C293 /* MVMCoreAlertHandler.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED2800254B0E0300A1C293 /* MVMCoreAlertHandler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27F7254B0E0200A1C293 /* MVMCoreAlertHandler+Extension.swift */; }; - D2ED2801254B0E0300A1C293 /* MVMCoreAlertOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED27F8254B0E0200A1C293 /* MVMCoreAlertOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED2802254B0E0300A1C293 /* MVMCoreAlertObject.m in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27F9254B0E0200A1C293 /* MVMCoreAlertObject.m */; }; - D2ED2803254B0E0300A1C293 /* MVMCoreAlertHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27FA254B0E0300A1C293 /* MVMCoreAlertHandler.m */; }; - D2ED280C254B0EB800A1C293 /* MVMCoreTopAlertAnimationDelegateProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED2805254B0EB700A1C293 /* MVMCoreTopAlertAnimationDelegateProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED280D254B0EB800A1C293 /* MVMCoreTopAlertOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED2806254B0EB700A1C293 /* MVMCoreTopAlertOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED280E254B0EB800A1C293 /* MVMCoreTopAlertOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = D2ED2807254B0EB700A1C293 /* MVMCoreTopAlertOperation.m */; }; - D2ED280F254B0EB800A1C293 /* MVMCoreTopAlertViewProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED2808254B0EB700A1C293 /* MVMCoreTopAlertViewProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED2810254B0EB800A1C293 /* MVMCoreTopAlertDelegateProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED2809254B0EB700A1C293 /* MVMCoreTopAlertDelegateProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED2811254B0EB800A1C293 /* MVMCoreTopAlertObject.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED280A254B0EB700A1C293 /* MVMCoreTopAlertObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED2812254B0EB800A1C293 /* MVMCoreTopAlertObject.m in Sources */ = {isa = PBXBuildFile; fileRef = D2ED280B254B0EB800A1C293 /* MVMCoreTopAlertObject.m */; }; - D2ED2815254B0EE400A1C293 /* MVMCoreGlobalTopAlertDelegateProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED2814254B0EE400A1C293 /* MVMCoreGlobalTopAlertDelegateProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D2ED2818254B115400A1C293 /* MVMCoreUIActionDelegateProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2ED2817254B112900A1C293 /* MVMCoreUIActionDelegateProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D2ED27FC254B0E0300A1C293 /* AlertObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2ED27F3254B0E0200A1C293 /* AlertObject.swift */; }; D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D12513EA6900564112 /* NotificationXButton.swift */; }; D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D32514F80C00564112 /* CollapsableNotification.swift */; }; D2FA83D62515021F00564112 /* CollapsableNotificationTopView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */; }; @@ -741,6 +717,8 @@ 187FEB292844D2A600BF29C2 /* VDSFormControlsTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSFormControlsTokens.xcframework; path = ../SharedFrameworks/VDSFormControlsTokens.xcframework; sourceTree = ""; }; 1D6D258626899B0B00DEBB08 /* ImageButtonModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageButtonModel.swift; path = MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift; sourceTree = SOURCE_ROOT; }; 1D6D258726899B0B00DEBB08 /* ImageButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageButton.swift; path = MVMCoreUI/Atomic/Atoms/Buttons/ImageButton.swift; sourceTree = SOURCE_ROOT; }; + 22B678F829E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNotificationAuthStatusBehavior.swift; sourceTree = ""; }; + 22B678FC29E82B0300CF4196 /* ListNotificationAuthModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListNotificationAuthModel.swift; sourceTree = ""; }; 27559EFB27D691D3000836C1 /* ViewMaskingProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewMaskingProtocol.swift; sourceTree = ""; }; 27577DCC286CA959001EC47E /* MoleculeMaskingProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeMaskingProtocol.swift; sourceTree = ""; }; 279B1568242BBC2F00921D6C /* ActionModelAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionModelAdapter.swift; sourceTree = ""; }; @@ -815,7 +793,6 @@ 9458C3152406C8FD00930963 /* UIFont+FontWrapping.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIFont+FontWrapping.h"; sourceTree = ""; }; 9458C3162406C8FD00930963 /* UIFont+FontWrapping.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIFont+FontWrapping.m"; sourceTree = ""; }; 948DB67D2326DCD90011F916 /* MultiProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiProgress.swift; sourceTree = ""; }; - 94C0150924215643005811A9 /* ActionTopAlertModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionTopAlertModel.swift; sourceTree = ""; }; 94C0150B2421564A005811A9 /* ActionCollapseNotificationModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionCollapseNotificationModel.swift; sourceTree = ""; }; 94C2D9832386F3F80006CF46 /* LabelAttributeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeModel.swift; sourceTree = ""; }; 94C2D9A023872BCC0006CF46 /* LabelAttributeUnderlineModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelAttributeUnderlineModel.swift; sourceTree = ""; }; @@ -890,15 +867,20 @@ AAE96FA125341F6A0037A989 /* ListStoreLocatorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStoreLocatorModel.swift; sourceTree = ""; }; AAE96FA425341F7D0037A989 /* ListStoreLocator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStoreLocator.swift; sourceTree = ""; }; AF1C33642883B5A4006B1001 /* ActionTopNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionTopNotificationHandler.swift; sourceTree = ""; }; - AF1C33662883B712006B1001 /* ActionPopupHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionPopupHandler.swift; sourceTree = ""; }; AF1C336828859778006B1001 /* ActionAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionAlertHandler.swift; sourceTree = ""; }; - AF1C336A28859C73006B1001 /* ActionTopAlertHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionTopAlertHandler.swift; sourceTree = ""; }; AF1C336C28859EE1006B1001 /* ActionOpenPanelHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionOpenPanelHandler.swift; sourceTree = ""; }; AF1C336E2885A16A006B1001 /* ActionCollapseNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionCollapseNotificationHandler.swift; sourceTree = ""; }; AF1C33702885AE76006B1001 /* MVMCoreUIActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUIActionHandler.swift; sourceTree = ""; }; AF1C33722885D481006B1001 /* MVMCoreUIActionOpenPageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUIActionOpenPageHandler.swift; sourceTree = ""; }; AF60A7F52892D2E300919EEB /* ActionDismissNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionDismissNotificationModel.swift; sourceTree = ""; }; AF60A7F72892D34D00919EEB /* ActionDismissNotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionDismissNotificationHandler.swift; sourceTree = ""; }; + AF766D252A3CD4C600749099 /* UIAccessibilityTraits+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAccessibilityTraits+Codable.swift"; sourceTree = ""; }; + AF7E509629E477C0009DC2AD /* AlertHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertHandler.swift; sourceTree = ""; }; + AF7E509729E477C0009DC2AD /* AlertController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertController.swift; sourceTree = ""; }; + AFA4931F29E5CA73001A9663 /* AlertOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertOperation.swift; sourceTree = ""; }; + AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; + AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVMCoreUILoggingDelegateProtocol.swift; sourceTree = ""; }; + AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertDelegateProtocol.swift; sourceTree = ""; }; AFE4A1D027DFB5EE00C458D0 /* VDSColorTokens.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VDSColorTokens.xcframework; path = ../SharedFrameworks/VDSColorTokens.xcframework; sourceTree = ""; }; AFE4A1D527DFBB6F00C458D0 /* UINavigationController+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Extension.swift"; sourceTree = ""; }; B4CC8FBC29DF34680005D28B /* Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Badge.swift; sourceTree = ""; }; @@ -958,8 +940,8 @@ D2092356244FA1EF0044AD09 /* ThreeLayerModelBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreeLayerModelBase.swift; sourceTree = ""; }; D20923582450ECE00044AD09 /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = ""; }; D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoButtonView.swift; sourceTree = ""; }; - D20C7008250BF99B0095B21C /* TopNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopNotificationModel.swift; sourceTree = ""; }; - D20C700A250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUITopAlertView+Extension.swift"; sourceTree = ""; }; + D20C7008250BF99B0095B21C /* NotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModel.swift; sourceTree = ""; }; + D20C700A250BFDE40095B21C /* NotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContainerView.swift; sourceTree = ""; }; D20F3B43252E00E4004B3F56 /* PageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageProtocol.swift; sourceTree = ""; }; D20FB164241A5D75004AFC3A /* NavigationItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModel.swift; sourceTree = ""; }; D213347623843825008E41B3 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; @@ -978,7 +960,7 @@ D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccordionMoleculeTableViewCell.swift; sourceTree = ""; }; D22D8392241C27B100D3DF69 /* BaseTemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTemplateModel.swift; sourceTree = ""; }; D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = ""; }; - D23118B225124E18001C8440 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; + D23118B225124E18001C8440 /* NotificationMoleculeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMoleculeView.swift; sourceTree = ""; }; D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinksModel.swift; sourceTree = ""; }; D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinks.swift; sourceTree = ""; }; D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTwoColumnPriceDescription.swift; sourceTree = ""; }; @@ -1069,16 +1051,6 @@ D29DF11421E6805F003B2FB9 /* NSLayoutConstraint+MFConvenience.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSLayoutConstraint+MFConvenience.m"; sourceTree = ""; }; D29DF11A21E684A9003B2FB9 /* MVMCoreUISplitViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUISplitViewController.h; sourceTree = ""; }; D29DF11B21E684A9003B2FB9 /* MVMCoreUISplitViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUISplitViewController.m; sourceTree = ""; }; - D29DF11F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUITopAlertMainView.h; sourceTree = ""; }; - D29DF12021E6851E003B2FB9 /* MVMCoreUITopAlertView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUITopAlertView.h; sourceTree = ""; }; - D29DF12121E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUITopAlertExpandableView.m; sourceTree = ""; }; - D29DF12221E6851E003B2FB9 /* MVMCoreUITopAlertShortView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUITopAlertShortView.h; sourceTree = ""; }; - D29DF12321E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUITopAlertBaseView.h; sourceTree = ""; }; - D29DF12421E6851E003B2FB9 /* MVMCoreUITopAlertView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUITopAlertView.m; sourceTree = ""; }; - D29DF12521E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUITopAlertMainView.m; sourceTree = ""; }; - D29DF12621E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUITopAlertShortView.m; sourceTree = ""; }; - D29DF12721E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreUITopAlertExpandableView.h; sourceTree = ""; }; - D29DF12821E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreUITopAlertBaseView.m; sourceTree = ""; }; D29DF13821E68636003B2FB9 /* MFStyler.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MFStyler.h; sourceTree = ""; }; D29DF13921E68637003B2FB9 /* MFStyler.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MFStyler.m; sourceTree = ""; }; D29DF14421E68728003B2FB9 /* MFSizeObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MFSizeObject.m; sourceTree = ""; }; @@ -1139,10 +1111,8 @@ D2C521A823EDE79E00CA2634 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; D2C78CD124228BBD00B69FDE /* ActionOpenPanelModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionOpenPanelModel.swift; sourceTree = ""; }; D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationXButtonModel.swift; sourceTree = ""; }; - D2CAC7CC251104FE00C75681 /* NotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModel.swift; sourceTree = ""; }; + D2CAC7CC251104FE00C75681 /* NotificationMoleculeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMoleculeModel.swift; sourceTree = ""; }; D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotificationModel.swift; sourceTree = ""; }; - D2CAC7D02511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUITopAlertMainView+Extension.swift"; sourceTree = ""; }; - D2CAC7D2251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUITopAlertExpandableView+Extension.swift"; sourceTree = ""; }; D2D2FCEF252B72AF0033EAAA /* MoleculeSectionFooterModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeSectionFooterModel.swift; sourceTree = ""; }; D2D2FCF2252B72CF0033EAAA /* MoleculeSectionFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeSectionFooter.swift; sourceTree = ""; }; D2D39579252FDBB300047B11 /* ModalSectionListTemplate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalSectionListTemplate.swift; sourceTree = ""; }; @@ -1171,27 +1141,9 @@ D2EC7BDC2527B83700F540AF /* SectionHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderFooterView.swift; sourceTree = ""; }; D2ED27E6254B0CE600A1C293 /* UIAlertActionStyle+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertActionStyle+Codable.swift"; sourceTree = ""; }; D2ED27E7254B0CE600A1C293 /* UIAlertControllerStyle+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIAlertControllerStyle+Extension.swift"; sourceTree = ""; }; - D2ED27E8254B0CE600A1C293 /* ActionPopupModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionPopupModel.swift; sourceTree = ""; }; D2ED27E9254B0CE600A1C293 /* ActionAlertModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionAlertModel.swift; sourceTree = ""; }; D2ED27EA254B0CE700A1C293 /* AlertModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertModel.swift; sourceTree = ""; }; - D2ED27F2254B0E0200A1C293 /* MVMCoreAlertDelegateProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreAlertDelegateProtocol.h; sourceTree = ""; }; - D2ED27F3254B0E0200A1C293 /* MVMCoreAlertObject+Swift.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MVMCoreAlertObject+Swift.swift"; sourceTree = ""; }; - D2ED27F4254B0E0200A1C293 /* MVMCoreAlertOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreAlertOperation.m; sourceTree = ""; }; - D2ED27F5254B0E0200A1C293 /* MVMCoreAlertObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreAlertObject.h; sourceTree = ""; }; - D2ED27F6254B0E0200A1C293 /* MVMCoreAlertHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreAlertHandler.h; sourceTree = ""; }; - D2ED27F7254B0E0200A1C293 /* MVMCoreAlertHandler+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MVMCoreAlertHandler+Extension.swift"; sourceTree = ""; }; - D2ED27F8254B0E0200A1C293 /* MVMCoreAlertOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreAlertOperation.h; sourceTree = ""; }; - D2ED27F9254B0E0200A1C293 /* MVMCoreAlertObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreAlertObject.m; sourceTree = ""; }; - D2ED27FA254B0E0300A1C293 /* MVMCoreAlertHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreAlertHandler.m; sourceTree = ""; }; - D2ED2805254B0EB700A1C293 /* MVMCoreTopAlertAnimationDelegateProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreTopAlertAnimationDelegateProtocol.h; sourceTree = ""; }; - D2ED2806254B0EB700A1C293 /* MVMCoreTopAlertOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreTopAlertOperation.h; sourceTree = ""; }; - D2ED2807254B0EB700A1C293 /* MVMCoreTopAlertOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreTopAlertOperation.m; sourceTree = ""; }; - D2ED2808254B0EB700A1C293 /* MVMCoreTopAlertViewProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreTopAlertViewProtocol.h; sourceTree = ""; }; - D2ED2809254B0EB700A1C293 /* MVMCoreTopAlertDelegateProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreTopAlertDelegateProtocol.h; sourceTree = ""; }; - D2ED280A254B0EB700A1C293 /* MVMCoreTopAlertObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreTopAlertObject.h; sourceTree = ""; }; - D2ED280B254B0EB800A1C293 /* MVMCoreTopAlertObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MVMCoreTopAlertObject.m; sourceTree = ""; }; - D2ED2814254B0EE400A1C293 /* MVMCoreGlobalTopAlertDelegateProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MVMCoreGlobalTopAlertDelegateProtocol.h; sourceTree = ""; }; - D2ED2817254B112900A1C293 /* MVMCoreUIActionDelegateProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MVMCoreUIActionDelegateProtocol.h; sourceTree = ""; }; + D2ED27F3254B0E0200A1C293 /* AlertObject.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertObject.swift; sourceTree = ""; }; D2FA83D12513EA6900564112 /* NotificationXButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationXButton.swift; sourceTree = ""; }; D2FA83D32514F80C00564112 /* CollapsableNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotification.swift; sourceTree = ""; }; D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotificationTopView.swift; sourceTree = ""; }; @@ -1441,6 +1393,7 @@ 27F97369246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift */, D23A900826125FFB007E14CE /* GetContactBehavior.swift */, D270E5662642F77300CDBED2 /* AddRemoveMoleculeBehavior.swift */, + 22B678F829E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift */, ); path = Behaviors; sourceTree = ""; @@ -1522,8 +1475,6 @@ 94C01508242155FE005811A9 /* Actions */ = { isa = PBXGroup; children = ( - 94C0150924215643005811A9 /* ActionTopAlertModel.swift */, - AF1C336A28859C73006B1001 /* ActionTopAlertHandler.swift */, 94C0150B2421564A005811A9 /* ActionCollapseNotificationModel.swift */, AF1C336E2885A16A006B1001 /* ActionCollapseNotificationHandler.swift */, D2C78CD124228BBD00B69FDE /* ActionOpenPanelModel.swift */, @@ -1531,8 +1482,6 @@ D2ED27E9254B0CE600A1C293 /* ActionAlertModel.swift */, D2ED27EA254B0CE700A1C293 /* AlertModel.swift */, AF1C336828859778006B1001 /* ActionAlertHandler.swift */, - D2ED27E8254B0CE600A1C293 /* ActionPopupModel.swift */, - AF1C33662883B712006B1001 /* ActionPopupHandler.swift */, C6687440259D92D400F32D13 /* ActionTopNotificationModel.swift */, AF1C33642883B5A4006B1001 /* ActionTopNotificationHandler.swift */, AF1C33722885D481006B1001 /* MVMCoreUIActionOpenPageHandler.swift */, @@ -1599,6 +1548,7 @@ children = ( AAE96FA125341F6A0037A989 /* ListStoreLocatorModel.swift */, AAE96FA425341F7D0037A989 /* ListStoreLocator.swift */, + 22B678FC29E82B0300CF4196 /* ListNotificationAuthModel.swift */, AA7F47722541AD560015A2C1 /* ListStarRatingModel.swift */, AA7F47752541AD6A0015A2C1 /* ListStarRating.swift */, ); @@ -1633,6 +1583,7 @@ EA985C862981AB0F00F2FF2E /* VDS-Tilelet+Codable.swift */, EA985C882981AB7100F2FF2E /* VDS-TextStyle.swift */, EA985C8A2983259900F2FF2E /* VDS-LabelAttributeModel.swift */, + AF766D252A3CD4C600749099 /* UIAccessibilityTraits+Codable.swift */, ); path = Extensions; sourceTree = ""; @@ -2043,7 +1994,7 @@ D29DF11921E68467003B2FB9 /* Containers */, D22D1F582204D2590077CEC0 /* Legacy */, D29DF10F21E67A7D003B2FB9 /* BaseControllers */, - D29DF11E21E6851E003B2FB9 /* TopAlert */, + D29DF11E21E6851E003B2FB9 /* Notification */, D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */, D29DF0D021E404D4003B2FB9 /* Info.plist */, ); @@ -2166,31 +2117,14 @@ path = Containers; sourceTree = ""; }; - D29DF11E21E6851E003B2FB9 /* TopAlert */ = { + D29DF11E21E6851E003B2FB9 /* Notification */ = { isa = PBXGroup; children = ( - D2ED2814254B0EE400A1C293 /* MVMCoreGlobalTopAlertDelegateProtocol.h */, - D2ED2805254B0EB700A1C293 /* MVMCoreTopAlertAnimationDelegateProtocol.h */, - D2ED2809254B0EB700A1C293 /* MVMCoreTopAlertDelegateProtocol.h */, - D2ED280A254B0EB700A1C293 /* MVMCoreTopAlertObject.h */, - D2ED280B254B0EB800A1C293 /* MVMCoreTopAlertObject.m */, - D2ED2806254B0EB700A1C293 /* MVMCoreTopAlertOperation.h */, - D2ED2807254B0EB700A1C293 /* MVMCoreTopAlertOperation.m */, - D2ED2808254B0EB700A1C293 /* MVMCoreTopAlertViewProtocol.h */, - D20C7008250BF99B0095B21C /* TopNotificationModel.swift */, - D20C700A250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift */, - D29DF12021E6851E003B2FB9 /* MVMCoreUITopAlertView.h */, - D29DF12421E6851E003B2FB9 /* MVMCoreUITopAlertView.m */, - D29DF12321E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.h */, - D29DF12821E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m */, - D29DF11F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.h */, - D29DF12521E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m */, - D29DF12221E6851E003B2FB9 /* MVMCoreUITopAlertShortView.h */, - D29DF12621E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m */, - D29DF12721E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.h */, - D29DF12121E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m */, + AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */, + D20C7008250BF99B0095B21C /* NotificationModel.swift */, + D20C700A250BFDE40095B21C /* NotificationContainerView.swift */, ); - path = TopAlert; + path = Notification; sourceTree = ""; }; D29DF13321E68604003B2FB9 /* Styles */ = { @@ -2340,11 +2274,11 @@ D29DF27821E7A533003B2FB9 /* MVMCoreUISession.m */, D29DF27321E79E81003B2FB9 /* MVMCoreUILoggingHandler.h */, D29DF27421E79E81003B2FB9 /* MVMCoreUILoggingHandler.m */, + AFA4933E29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift */, D2C5001621F8ECDD001DA659 /* MVMCoreUIViewControllerMappingObject.h */, D2C5001721F8ECDD001DA659 /* MVMCoreUIViewControllerMappingObject.m */, D2092352244F7D630044AD09 /* MVMCoreUIViewControllerMappingObject+Extension.swift */, D296E14622A597490051EBE7 /* MVMCoreUIViewConstrainingProtocol.h */, - D2ED2817254B112900A1C293 /* MVMCoreUIActionDelegateProtocol.h */, AF1C33702885AE76006B1001 /* MVMCoreUIActionHandler.swift */, D23A90672614B0B4007E14CE /* CoreUIModelMapping.swift */, ); @@ -2496,13 +2430,11 @@ children = ( D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */, D2FA83D12513EA6900564112 /* NotificationXButton.swift */, - D2CAC7CC251104FE00C75681 /* NotificationModel.swift */, - D23118B225124E18001C8440 /* Notification.swift */, - D2CAC7D02511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift */, + D2CAC7CC251104FE00C75681 /* NotificationMoleculeModel.swift */, + D23118B225124E18001C8440 /* NotificationMoleculeView.swift */, D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */, D2FA83D32514F80C00564112 /* CollapsableNotification.swift */, D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */, - D2CAC7D2251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift */, ); path = TopNotification; sourceTree = ""; @@ -2538,15 +2470,11 @@ D2ED27D8254B0C1F00A1C293 /* Alerts */ = { isa = PBXGroup; children = ( - D2ED27F2254B0E0200A1C293 /* MVMCoreAlertDelegateProtocol.h */, - D2ED27F6254B0E0200A1C293 /* MVMCoreAlertHandler.h */, - D2ED27FA254B0E0300A1C293 /* MVMCoreAlertHandler.m */, - D2ED27F7254B0E0200A1C293 /* MVMCoreAlertHandler+Extension.swift */, - D2ED27F5254B0E0200A1C293 /* MVMCoreAlertObject.h */, - D2ED27F9254B0E0200A1C293 /* MVMCoreAlertObject.m */, - D2ED27F3254B0E0200A1C293 /* MVMCoreAlertObject+Swift.swift */, - D2ED27F8254B0E0200A1C293 /* MVMCoreAlertOperation.h */, - D2ED27F4254B0E0200A1C293 /* MVMCoreAlertOperation.m */, + AFA4935629EE3DCC001A9663 /* AlertDelegateProtocol.swift */, + D2ED27F3254B0E0200A1C293 /* AlertObject.swift */, + AF7E509729E477C0009DC2AD /* AlertController.swift */, + AF7E509629E477C0009DC2AD /* AlertHandler.swift */, + AFA4931F29E5CA73001A9663 /* AlertOperation.swift */, ); path = Alerts; sourceTree = ""; @@ -2582,7 +2510,6 @@ D29DF25921E6A22D003B2FB9 /* MFButtonProtocol.h in Headers */, D29DF28421E7AB24003B2FB9 /* MVMCoreUICommonViewsUtility.h in Headers */, D29DF2CE21E7C104003B2FB9 /* MFLoadingViewController.h in Headers */, - D29DF12A21E6851E003B2FB9 /* MVMCoreUITopAlertView.h in Headers */, D29DF27521E79E81003B2FB9 /* MVMCoreUILoggingHandler.h in Headers */, D29DF2B321E7B76D003B2FB9 /* MFLoadingSpinner.h in Headers */, D20492A424329A2800A5EED6 /* MVMCoreUIPagingProtocol.h in Headers */, @@ -2591,29 +2518,14 @@ D29DF26E21E6AA0B003B2FB9 /* FLAnimatedImage.h in Headers */, D29DF11621E6805F003B2FB9 /* NSLayoutConstraint+MFConvenience.h in Headers */, 01E569D3223FFFA500327251 /* ThreeLayerViewController.swift in Headers */, - D29DF13121E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.h in Headers */, D29DF2CA21E7BFC8003B2FB9 /* MFSizeThreshold.h in Headers */, D29DF28021E7AA51003B2FB9 /* MVMCoreUIDetailViewProtocol.h in Headers */, D29DF2EE21ECEADF003B2FB9 /* MFFonts.h in Headers */, - D29DF12D21E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.h in Headers */, D2C5001821F8ECDD001DA659 /* MVMCoreUIViewControllerMappingObject.h in Headers */, - D2ED2811254B0EB800A1C293 /* MVMCoreTopAlertObject.h in Headers */, - D2ED27FE254B0E0300A1C293 /* MVMCoreAlertObject.h in Headers */, - D2ED280F254B0EB800A1C293 /* MVMCoreTopAlertViewProtocol.h in Headers */, - D2ED280C254B0EB800A1C293 /* MVMCoreTopAlertAnimationDelegateProtocol.h in Headers */, - D2ED280D254B0EB800A1C293 /* MVMCoreTopAlertOperation.h in Headers */, - D2ED2801254B0E0300A1C293 /* MVMCoreAlertOperation.h in Headers */, - D2ED2818254B115400A1C293 /* MVMCoreUIActionDelegateProtocol.h in Headers */, - D2ED27FB254B0E0300A1C293 /* MVMCoreAlertDelegateProtocol.h in Headers */, - D2ED2810254B0EB800A1C293 /* MVMCoreTopAlertDelegateProtocol.h in Headers */, - D2ED27FF254B0E0300A1C293 /* MVMCoreAlertHandler.h in Headers */, - D2ED2815254B0EE400A1C293 /* MVMCoreGlobalTopAlertDelegateProtocol.h in Headers */, D29DF26F21E6AA0B003B2FB9 /* FLAnimatedImageView.h in Headers */, D29DF2A121E7AF4E003B2FB9 /* MVMCoreUIUtility.h in Headers */, D29DF2C821E7BFC1003B2FB9 /* MFSizeObject.h in Headers */, D29DF2E121E9240B003B2FB9 /* MVMCoreUIPanelProtocol.h in Headers */, - D29DF12921E6851E003B2FB9 /* MVMCoreUITopAlertMainView.h in Headers */, - D29DF12C21E6851E003B2FB9 /* MVMCoreUITopAlertShortView.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2726,7 +2638,7 @@ AAB9C10824346F4B00151545 /* RadioSwatches.swift in Sources */, 94C2D9A923872E5E0006CF46 /* LabelAttributeImageModel.swift in Sources */, DBC4391922442197001AB423 /* DashLine.swift in Sources */, - D2ED27FC254B0E0300A1C293 /* MVMCoreAlertObject+Swift.swift in Sources */, + D2ED27FC254B0E0300A1C293 /* AlertObject.swift in Sources */, D264FAAA2440F97600D98315 /* CollectionView.swift in Sources */, AFE4A1D627DFBB6F00C458D0 /* UINavigationController+Extension.swift in Sources */, AAC23FAD24D92A0D009208DF /* ListThreeColumnSpeedTestModel.swift in Sources */, @@ -2772,17 +2684,15 @@ 011D95A924057AC7000E3791 /* FormGroupWatcherFieldProtocol.swift in Sources */, EA985C892981AB7100F2FF2E /* VDS-TextStyle.swift in Sources */, BB2BF0EA2452A9BB001D0FC2 /* ListDeviceComplexButtonSmall.swift in Sources */, - D20C700B250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift in Sources */, + D20C700B250BFDE40095B21C /* NotificationContainerView.swift in Sources */, D236E5B4241FEB1000C38625 /* ListTwoColumnPriceDescription.swift in Sources */, 0AA33B3A2398524F0067DD0F /* Toggle.swift in Sources */, EA7E67742758310500ABF773 /* EnableFormFieldEffectModel.swift in Sources */, - D29DF12F21E6851E003B2FB9 /* MVMCoreUITopAlertMainView.m in Sources */, 942C378C2412F4FA0066E45E /* ModalMoleculeListTemplate.swift in Sources */, BB47A588241615FA002BB23C /* ListOneColumnFullWidthTextDividerSubsection.swift in Sources */, 012A88C8238DB02000FE3DA1 /* MoleculeDelegateProtocol.swift in Sources */, 8D8067D12444472F00203BE8 /* ListRightVariablePriceChangeAllTextAndLinksModel.swift in Sources */, 0A7EF86123D8AC2500B2AAD1 /* DigitEntryFieldModel.swift in Sources */, - D2ED2802254B0E0300A1C293 /* MVMCoreAlertObject.m in Sources */, D224798C231450C8003FCCF9 /* HeadlineBodyToggle.swift in Sources */, 9458C3182406C8FD00930963 /* UIFont+FontWrapping.m in Sources */, 522679C123FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinks.swift in Sources */, @@ -2801,7 +2711,6 @@ AAB9C10A243496DD00151545 /* RadioSwatch.swift in Sources */, D29DF2B421E7B76D003B2FB9 /* MFLoadingSpinner.m in Sources */, 011D9602240DA20A000E3791 /* FormRuleWatcherFieldProtocol.swift in Sources */, - AF1C33672883B712006B1001 /* ActionPopupHandler.swift in Sources */, D23A900926125FFB007E14CE /* GetContactBehavior.swift in Sources */, D264FAA1243CF66B00D98315 /* ContainerCollectionReusableView.swift in Sources */, AA617AB22453012400910B8F /* ListDeviceComplexLinkSmallModel.swift in Sources */, @@ -2809,7 +2718,6 @@ D2E2A99823D8D63C000B42E6 /* ActionDetailWithImageModel.swift in Sources */, D28764AC245898A400CB882D /* ThreeLayerFillMiddleTemplateModel.swift in Sources */, BBBBC87D24374A4900B0F079 /* ListThreeColumnBillChangesDividerModel.swift in Sources */, - D2ED2800254B0E0300A1C293 /* MVMCoreAlertHandler+Extension.swift in Sources */, D2ED27EE254B0CE700A1C293 /* ActionAlertModel.swift in Sources */, D2E2A99D23DA3217000B42E6 /* UIStackViewAlignment+Extension.swift in Sources */, 01EB369423609801006832FA /* HeadlineBodyModel.swift in Sources */, @@ -2820,11 +2728,9 @@ 0A21DB7F235DECC500C160A2 /* EntryField.swift in Sources */, D2E2A99F23E07F8A000B42E6 /* PillButton.swift in Sources */, D2C5001921F8ECDD001DA659 /* MVMCoreUIViewControllerMappingObject.m in Sources */, - D29DF12E21E6851E003B2FB9 /* MVMCoreUITopAlertView.m in Sources */, AA1EC59724373985003D6F50 /* ListThreeColumnSpeedTestDividerModel.swift in Sources */, D23A8FEB26122F69007E14CE /* VisibleBehaviorForVideoModel.swift in Sources */, BB1D17E0244EAA30001D2002 /* ListDeviceComplexButtonMediumModel.swift in Sources */, - D2CAC7D3251105A700C75681 /* MVMCoreUITopAlertExpandableView+Extension.swift in Sources */, AA07EA932510A451009A2AE3 /* Star.swift in Sources */, D29DF2CF21E7C104003B2FB9 /* MFLoadingViewController.m in Sources */, D28A837B23C928DA00DFE4FC /* MoleculeListCellProtocol.swift in Sources */, @@ -2851,7 +2757,6 @@ 52B201D324081CFB00D2011E /* ListLeftVariableRadioButtonAndPaymentMethodModel.swift in Sources */, 525239C02407BCFF00454969 /* ListTwoColumnPriceDetailsModel.swift in Sources */, D2E2A99A23D8D6B4000B42E6 /* HeadlineBodyButtonModel.swift in Sources */, - D2ED280E254B0EB800A1C293 /* MVMCoreTopAlertOperation.m in Sources */, D202AFE6242A6A9C00E5BEDF /* UICollectionViewScrollPosition+Extension.swift in Sources */, D20F3B44252E00E4004B3F56 /* PageProtocol.swift in Sources */, AA37CBD3251907200027344C /* StarsModel.swift in Sources */, @@ -2863,8 +2768,6 @@ 0A0FEC7425D42A5E00AF2548 /* BaseItemPickerEntryField.swift in Sources */, D29DF2A221E7AF4E003B2FB9 /* MVMCoreUIUtility.m in Sources */, D23A8FF82612308D007E14CE /* PageBehaviorProtocolRequirer.swift in Sources */, - D29DF12B21E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m in Sources */, - D2ED27ED254B0CE700A1C293 /* ActionPopupModel.swift in Sources */, 94C2D9A723872DA90006CF46 /* LabelAttributeColorModel.swift in Sources */, 943820842432382400B43AF3 /* WebView.swift in Sources */, 0103B84E23D7E33A009C315C /* HeadlineBodyToggleModel.swift in Sources */, @@ -2878,6 +2781,7 @@ 8DDD6C1D244D90B8006A2232 /* ListThreeColumnDataUsage.swift in Sources */, 0A849EFE246F1775009F277F /* RuleEqualsIgnoreCaseModel.swift in Sources */, D28764FB245A33A500CB882D /* TwoLinkViewModel.swift in Sources */, + AFA4932029E5CA73001A9663 /* AlertOperation.swift in Sources */, AAA74A192410C05800080241 /* HeadersH2NoButtonsBodyTextModel.swift in Sources */, AAE96FA525341F7D0037A989 /* ListStoreLocator.swift in Sources */, D282AABA224131D100C46919 /* MFTransparentGIFView.swift in Sources */, @@ -2899,6 +2803,7 @@ D28A838323CCBD3F00DFE4FC /* WheelModel.swift in Sources */, D268C70C2386DFFD007F2C1C /* MoleculeStackItemModel.swift in Sources */, 0A9D091D2433796500D2E6C0 /* BarsCarouselIndicatorModel.swift in Sources */, + AFA4933F29E874F0001A9663 /* MVMCoreUILoggingDelegateProtocol.swift in Sources */, DBEFFA04225A829700230692 /* Label.swift in Sources */, D2D6CD4022E78C1A00D701B8 /* Scroller.swift in Sources */, AF1C33712885AE76006B1001 /* MVMCoreUIActionHandler.swift in Sources */, @@ -2911,7 +2816,6 @@ 01509D952327ED1900EF99AA /* HeadlineBodyLinkToggle.swift in Sources */, AA104ADA244734DB004D2810 /* HeadersH1LandingPageHeader.swift in Sources */, 31BE15CB23D8924D00452370 /* CheckboxLabelModel.swift in Sources */, - D29DF13021E6851E003B2FB9 /* MVMCoreUITopAlertShortView.m in Sources */, 94F6516D2437954100631BF9 /* Tabs.swift in Sources */, 5248BFEC23F12E350059236A /* ListThreeColumnPlanDataDivider.swift in Sources */, 0ABD136D237CAD1E0081388D /* DateDropdownEntryField.swift in Sources */, @@ -2921,8 +2825,7 @@ 8DEFA95C243DAC20000D27E5 /* ListThreeColumnDataUsageDividerModel.swift in Sources */, D2092357244FA1EF0044AD09 /* ThreeLayerModelBase.swift in Sources */, D2FD4A4925199BD9000C28A9 /* AccessibilityProtocol.swift in Sources */, - D2CAC7CD251104FE00C75681 /* NotificationModel.swift in Sources */, - D2ED2803254B0E0300A1C293 /* MVMCoreAlertHandler.m in Sources */, + D2CAC7CD251104FE00C75681 /* NotificationMoleculeModel.swift in Sources */, 0A1B4A96233BB18F005B3FB4 /* CheckboxLabel.swift in Sources */, EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */, D20923592450ECE00044AD09 /* TableView.swift in Sources */, @@ -2939,6 +2842,7 @@ 0A7EF86723D8B0AE00B2AAD1 /* DateDropdownEntryFieldModel.swift in Sources */, 444FB7C32821B76B00DFE692 /* TitleLockupModel.swift in Sources */, D29C94D5242901C9003813BA /* MVMCoreUICommonViewsUtility+Extension.swift in Sources */, + AF766D262A3CD4C600749099 /* UIAccessibilityTraits+Codable.swift in Sources */, D260105323CEA61600764D80 /* ToggleModel.swift in Sources */, 014AA72523C501E2006F3E93 /* ContainerModel.swift in Sources */, 0A7EF86523D8AFFF00B2AAD1 /* ItemDropdownEntryFieldModel.swift in Sources */, @@ -2977,6 +2881,7 @@ AA7F47732541AD560015A2C1 /* ListStarRatingModel.swift in Sources */, AA7F47762541AD6A0015A2C1 /* ListStarRating.swift in Sources */, 0A41BA7F23453A6400D4C0BC /* TextEntryField.swift in Sources */, + AF7E509829E477C1009DC2AD /* AlertHandler.swift in Sources */, D2ED27EB254B0CE700A1C293 /* UIAlertActionStyle+Codable.swift in Sources */, BB55B51D244482C1002001AD /* ListRightVariablePriceChangeBodyText.swift in Sources */, 017BEB382360C6AC0024EF95 /* RadioButtonLabel.swift in Sources */, @@ -3006,12 +2911,12 @@ D2E2A99623D8CF85000B42E6 /* HeadlineBodyLinkToggleModel.swift in Sources */, C6FA7D5323C77A4A00A3614A /* StringAndMoleculeStack.swift in Sources */, 32F8804624765C6E00C2ACB3 /* ListLeftVariableNumberedListAllTextAndLinksModel.swift in Sources */, - AF1C336B28859C73006B1001 /* ActionTopAlertHandler.swift in Sources */, 011D958524042432000E3791 /* RulesProtocol.swift in Sources */, 4457904E27ECE989002B1E1E /* UIImageRenderingMode+Extension.swift in Sources */, - D23118B325124E18001C8440 /* Notification.swift in Sources */, + D23118B325124E18001C8440 /* NotificationMoleculeView.swift in Sources */, AA9972502475309F00FC7472 /* ListLeftVariableIconAllTextLinksModel.swift in Sources */, AA69AAF62445BF5700AF3D3B /* ListLeftVariableCheckboxBodyText.swift in Sources */, + AFA4935729EE3DCC001A9663 /* AlertDelegateProtocol.swift in Sources */, D264FAA3243E632F00D98315 /* ProgrammaticCollectionViewController.swift in Sources */, D29DF27A21E7A533003B2FB9 /* MVMCoreUISession.m in Sources */, 27F9736A246750BE00CAB5C5 /* ScreenBrightnessModifierBehavior.swift in Sources */, @@ -3022,7 +2927,6 @@ 3265B30224BCA737000D154B /* HeadersH1NoButtonsBodyTextModel.swift in Sources */, D28A838F23CCDEDE00DFE4FC /* TwoButtonViewModel.swift in Sources */, D264FAAC2441009400D98315 /* RadioBoxCollectionViewCell.swift in Sources */, - D2ED27FD254B0E0300A1C293 /* MVMCoreAlertOperation.m in Sources */, BB2C969224330F73006FF80C /* ListRightVariableTextLinkAllTextAndLinks.swift in Sources */, D2D90B42240463E100DD6EC9 /* MoleculeHeaderModel.swift in Sources */, 012A88B1238C880100FE3DA1 /* CarouselPagingModelProtocol.swift in Sources */, @@ -3048,15 +2952,17 @@ 324FB6AC24936717002552C7 /* ListLeftVariableNumberedListBodyText.swift in Sources */, AAA74A172410C04600080241 /* HeadersH2NoButtonsBodyText.swift in Sources */, 522679C223FE886900906CBA /* ListLeftVariableCheckboxAllTextAndLinksModel.swift in Sources */, + 22B678FD29E82B0300CF4196 /* ListNotificationAuthModel.swift in Sources */, AA7F32AB246C0F7900C965BA /* ListLeftVariableRadioButtonAllTextAndLinksModel.swift in Sources */, 8D084AD02410BF4800951227 /* ListOneColumnFullWidthTextBodyTextModel.swift in Sources */, AF60A7F82892D34D00919EEB /* ActionDismissNotificationHandler.swift in Sources */, D253BB9E2458751F002DE544 /* BGImageMoleculeModel.swift in Sources */, AA104AC924472DC7004D2810 /* HeadersH1ButtonModel.swift in Sources */, 0ABD1371237DB0450081388D /* ItemDropdownEntryField.swift in Sources */, - D20C7009250BF99B0095B21C /* TopNotificationModel.swift in Sources */, + D20C7009250BF99B0095B21C /* NotificationModel.swift in Sources */, D29C558A25C05C7D0082E7D6 /* BGVideoImageMoleculeModel.swift in Sources */, 8D24041123E7FB9E009E23BE /* ListLeftVariableIconWithRightCaret.swift in Sources */, + AFA4932229E5EF2E001A9663 /* NotificationHandler.swift in Sources */, BB2FB3BD247E7EF200DF73CD /* Tags.swift in Sources */, AA104ADC244734EA004D2810 /* HeadersH1LandingPageHeaderModel.swift in Sources */, BBAA4F03243D8E3B005AAD5F /* RadioBoxes.swift in Sources */, @@ -3078,13 +2984,13 @@ D2FA83D22513EA6900564112 /* NotificationXButton.swift in Sources */, D2D90B442404789000DD6EC9 /* MoleculeContainerProtocol.swift in Sources */, 0A7ECC5F243CEB1200C828E8 /* ColorViewWithLabel.swift in Sources */, - 94C0150A24215643005811A9 /* ActionTopAlertModel.swift in Sources */, BB3BC12F2550094500297977 /* ListLeftVariableIconWithRightCaretAllTextLinks.swift in Sources */, 012A88DB238ED45900FE3DA1 /* CarouselModel.swift in Sources */, D2092355244FA0FD0044AD09 /* ThreeLayerTemplateModelProtocol.swift in Sources */, 0AE14F64238315D2005417F8 /* TextField.swift in Sources */, 0A51F3E22475CB73002E08B6 /* LoadingSpinnerModel.swift in Sources */, D2169303251E53D9002A6324 /* SectionListTemplateModel.swift in Sources */, + AF7E509929E477C1009DC2AD /* AlertController.swift in Sources */, 0AB764D124460F6300E7FE72 /* UIDatePicker+Extension.swift in Sources */, BB105859248DEFF70069D008 /* UICollectionViewLeftAlignedLayout.swift in Sources */, D253BB9C245874F8002DE544 /* BGImageMolecule.swift in Sources */, @@ -3116,12 +3022,12 @@ AA45AA0D24BF0276007A6EA7 /* LockUpsPlanNames.swift in Sources */, 8DE5BECF2456F7B100772E76 /* ListTwoColumnDropdownSelectors.swift in Sources */, D2E1FADF2268B8E700AEFD8C /* ThreeLayerTableViewController.swift in Sources */, - D2CAC7D12511058C00C75681 /* MVMCoreUITopAlertMainView+Extension.swift in Sources */, 0A21DB83235DFBC500C160A2 /* MdnEntryField.swift in Sources */, 0AE98BB723FF18E9004C5109 /* ArrowModel.swift in Sources */, 01F2C20427C81F9700DC3D36 /* SubNavInteractor.swift.swift in Sources */, D28A837D23CCA86A00DFE4FC /* TabsListItemModel.swift in Sources */, 0A51F3E32475CB73002E08B6 /* LoadingSpinner.swift in Sources */, + 22B678F929E7944E00CF4196 /* GetNotificationAuthStatusBehavior.swift in Sources */, BB2FB3BB247E7EBC00DF73CD /* TagCollectionViewCell.swift in Sources */, 012A88C6238DA34000FE3DA1 /* ModuleMoleculeModel.swift in Sources */, B4CC8FBF29DF34730005D28B /* BadgeModel.swift in Sources */, @@ -3176,7 +3082,6 @@ EAA0CFB1275E823A00D65EB0 /* HideFormFieldEffectModel.swift in Sources */, D23A8FFB26123189007E14CE /* PageBehaviorModelProtocol.swift in Sources */, 52267A0723FFE25000906CBA /* ListOneColumnFullWidthTextAllTextAndLinks.swift in Sources */, - D2ED2812254B0EB800A1C293 /* MVMCoreTopAlertObject.m in Sources */, 0AA4D2E125CAEC72008DB32D /* AccessibilityModelProtocol.swift in Sources */, EA05EFA9278DDE2C00828819 /* ClearFormFieldEffectModel.swift in Sources */, C003506123AA94CD00B6AC29 /* Button.swift in Sources */, @@ -3191,7 +3096,6 @@ D21B7F73243BAC6800051ABF /* CollectionItemModelProtocol.swift in Sources */, AA104B1A24474A66004D2810 /* HeadersH2Buttons.swift in Sources */, C7192E7D23C301750050C2A0 /* HeadLineBodyCaretLinkImage.swift in Sources */, - D29DF13221E6851E003B2FB9 /* MVMCoreUITopAlertBaseView.m in Sources */, D2D2FCF0252B72AF0033EAAA /* MoleculeSectionFooterModel.swift in Sources */, BB1D17E2244EAA46001D2002 /* ListDeviceComplexButtonMedium.swift in Sources */, D2FA83D42514F80C00564112 /* CollapsableNotification.swift in Sources */, diff --git a/MVMCoreUI/Alerts/AlertController.swift b/MVMCoreUI/Alerts/AlertController.swift new file mode 100644 index 00000000..3119face --- /dev/null +++ b/MVMCoreUI/Alerts/AlertController.swift @@ -0,0 +1,38 @@ +// +// AlertController.swift +// MVMCore +// +// Created by Scott Pfeil on 3/24/23. +// Copyright © 2023 myverizon. All rights reserved. +// + +import Foundation +import MVMCore + +public class AlertController: UIAlertController { + @objc dynamic public var visible = false + private let visibleKey = "isVisible" + + public override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return MVMCoreGetterUtility.isOnIPad() ? .all : .portrait + } + + public override var description: String { + return "\(super.description)|title=\(title ?? "")|message=\(message ?? "")" + } + + public override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + willChangeValue(forKey: visibleKey) + visible = true + didChangeValue(forKey: visibleKey) + } + + public override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + willChangeValue(forKey: visibleKey) + visible = false + didChangeValue(forKey: visibleKey) + } +} + diff --git a/MVMCoreUI/Alerts/AlertDelegateProtocol.swift b/MVMCoreUI/Alerts/AlertDelegateProtocol.swift new file mode 100644 index 00000000..4b032f78 --- /dev/null +++ b/MVMCoreUI/Alerts/AlertDelegateProtocol.swift @@ -0,0 +1,19 @@ +// +// AlertDelegateProtocol.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 4/17/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation + +@objc +public protocol AlertDelegateProtocol { + // All are performed on the main thread. + @MainActor func alertShown(_ alertController: UIAlertController) + @MainActor func alertCancelled(_ alertController: UIAlertController) + @MainActor func alertDismissed(_ alertController: UIAlertController) + @MainActor func alertPaused(_ alertController: UIAlertController) + @MainActor func alertUnpaused(_ alertController: UIAlertController) +} diff --git a/MVMCoreUI/Alerts/AlertHandler.swift b/MVMCoreUI/Alerts/AlertHandler.swift new file mode 100644 index 00000000..51b7433a --- /dev/null +++ b/MVMCoreUI/Alerts/AlertHandler.swift @@ -0,0 +1,96 @@ +// +// AlertHandler.swift +// MVMCore +// +// Created by Scott Pfeil on 4/10/23. +// Copyright © 2023 myverizon. All rights reserved. +// + +import MVMCore + +public class AlertHandler { + + /// Returns the handler stored in the CoreUIObject + public static func shared() -> Self { + return MVMCoreActionUtility.fatalClassCheck(object: CoreUIObject.sharedInstance()?.alertHandler) + } + + /// The operation queue of alert operations. + private var queue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + + public init() {} + + /// Returns if an alert is currently showing in the hierarchy, even if it is not the top presented view. + public func isAlertShowing() -> Bool { + return queue.operations.contains(where: { operation in + return !operation.isCancelled && + !operation.isFinished && + operation.isExecuting + }) + } + + /// Returns if a greedy alert is currently showing in the hierarchy, even if it is not the top presented view. + public func isGreedyAlertShowing() -> Bool { + return queue.operations.contains(where: { operation in + return !operation.isCancelled && + !operation.isFinished && + operation.isExecuting && + (operation as? AlertOperation)?.alertObject.isGreedy ?? false + }) + } + + @MainActor + public func createAlertController(with alertModel: AlertModelProtocol) -> AlertController { + // ActionSheets are not supported on iPad interfaces without a source rect (i.e. a source element) which isn't currently supported for our generic handling. + // TODO: Find a way to support this. + var alertStyle = alertModel.preferredStyle + if alertStyle == .actionSheet, UIDevice.current.userInterfaceIdiom != .phone { + alertStyle = .alert + } + + // Create the alert. Adds the actions one by one. + let alertController = AlertController(title: alertModel.title, message: alertModel.message, preferredStyle: alertStyle) + for action in alertModel.actions { + alertController.addAction(action) + } + if let index = alertModel.preferredActionIndex { + alertController.preferredAction = alertModel.actions[index] + } + return alertController + } + + /// Shows an alert using the alert object. + @MainActor + public func queueAlertToShow(with alertObject: AlertObject) -> UIAlertController { + + // It's a greedy alert! Clear all alerts that are queued up and the one that is showing + if alertObject.isGreedy { + removeAllAlertViews() + } + + let alertController = createAlertController(with: alertObject.alertModel) + let alertOperation = AlertOperation(with: alertController, alertObject: alertObject) + queue.addOperation(alertOperation) + return alertController + } + + /** Iterates through all scheduled alerts and cancels any that match the provided predicate. + * @param predicate The predicate block to decide whether to cancel an alert. + */ + public func cancelAlert(using predicate: ((AlertObject) -> Bool)) { + for case let operation as AlertOperation in queue.operations { + if predicate(operation.alertObject) { + operation.cancel() + } + } + } + + /// Cancels all current alerts + public func removeAllAlertViews() { + queue.cancelAllOperations() + } +} diff --git a/MVMCoreUI/Alerts/AlertObject.swift b/MVMCoreUI/Alerts/AlertObject.swift new file mode 100644 index 00000000..1cafe734 --- /dev/null +++ b/MVMCoreUI/Alerts/AlertObject.swift @@ -0,0 +1,35 @@ +// +// AlertObject.swift +// MVMCore +// +// Created by Suresh, Kamlesh on 7/10/20. +// Copyright © 2020 myverizon. All rights reserved. +// + +import MVMCore + +public protocol AlertModelProtocol { + var title: String? { get } + var message: String? { get } + var actions: [UIAlertAction] { get } + var preferredActionIndex: Int? { get } + var preferredStyle: UIAlertController.Style { get } +} + +/// An object with properties for managing the alert. +public struct AlertObject { + + /// Greedy alerts dismiss any other alerts and do not allow any other alerts to show until finished. + public var isGreedy = false + + /// The alert model for the alert to show. + public var alertModel: AlertModelProtocol + + public weak var alertDelegate: AlertDelegateProtocol? + + public init(alertModel: AlertModelProtocol, isGreedy: Bool = false, alertDelegate: AlertDelegateProtocol? = nil) { + self.alertModel = alertModel + self.isGreedy = isGreedy + self.alertDelegate = alertDelegate + } +} diff --git a/MVMCoreUI/Alerts/AlertOperation.swift b/MVMCoreUI/Alerts/AlertOperation.swift new file mode 100644 index 00000000..088a6ff5 --- /dev/null +++ b/MVMCoreUI/Alerts/AlertOperation.swift @@ -0,0 +1,138 @@ +// +// AlertOperation.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 4/11/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import MVMCore +import Dispatch +import Combine + +public class AlertOperation: MVMCoreOperation { + + private actor Properties { + private var isDisplayed: Bool = false + + func set(displayed: Bool) { + isDisplayed = displayed + } + + func getIsDisplayed() -> Bool { + return isDisplayed + } + } + private var properties = Properties() + + public let alertController: AlertController + + public let alertObject: AlertObject + + /// For tracking isVisible changes of the alert controller. + private var cancellable: Cancellable? + + /// Blocks the navigation queue to ensure no other navigation happens while an alert is displayed. + private var blockingOperation: MVMCoreOperation? + + public init(with alert: AlertController, alertObject: AlertObject) { + self.alertController = alert + self.alertObject = alertObject + super.init() + } + + deinit { + stopObservingAlertView() + } + + public override func main() { + guard !checkAndHandleForCancellation() else { return } + + // Observe for when it is removed. + observeForCurrentAlertViewDismissal() + + Task(priority: .high) { + guard let viewControllerToPresentOn = await NavigationHandler.shared().getViewControllerToPresentOn() else { + markAsFinished() + return + } + + // Presents the alert. + let presentationOperation = await NavigationOperation(with: .present(viewController: alertController, onController: viewControllerToPresentOn), tryToReplace: false) + + let blockingOperation = MVMCoreOperation() + blockingOperation.addDependency(presentationOperation) + self.blockingOperation = blockingOperation + + // Block other navigation until this alert is removed. + NavigationHandler.shared().navigationQueue.addOperation(blockingOperation) + + await NavigationHandler.shared().navigate(with: presentationOperation) + + // We finished but it was not displayed yet. It's possible that it was cancelled. Finish this task + if await !self.properties.getIsDisplayed() { + self.markAsFinished() + } else { + (CoreUIObject.sharedInstance()?.loggingDelegate as? MVMCoreUILoggingDelegateProtocol)?.logAlert(with: self.alertObject) + if self.isCancelled { + await self.dismissAlertView() + } + } + } + } + + public override func cancel() { + super.cancel() + Task { @MainActor in + self.alertObject.alertDelegate?.alertCancelled(self.alertController) + await self.dismissAlertView() + } + } + + private func dismissAlertView() async { + guard await properties.getIsDisplayed() else { return } + await withCheckedContinuation { continuation in + Task { + let dismissOperation = await NavigationOperation(with: .dismiss(viewController: alertController)) + dismissOperation.queuePriority = .veryHigh + let task = Task(priority: .high) { await NavigationHandler.shared().navigate(with: dismissOperation) } + blockingOperation?.markAsFinished() + _ = await task.result + continuation.resume() + } + } + } + + public override func markAsFinished() { + blockingOperation?.markAsFinished() + super.markAsFinished() + } + + // MARK: Observer Functions + + private func observeForCurrentAlertViewDismissal() { + stopObservingAlertView() + cancellable = alertController.publisher(for: \AlertController.visible).sink() { [weak self] visible in + guard let self = self else { return } + Task { + guard await self.properties.getIsDisplayed() != visible else { return } + await self.properties.set(displayed: visible) + Task { @MainActor in + if visible { + self.alertObject.alertDelegate?.alertShown(self.alertController) + } else { + self.alertObject.alertDelegate?.alertDismissed(self.alertController) + + // Is visible was set to NO, meaning that the alertview is no longer visible. + self.stopObservingAlertView() + self.markAsFinished() + } + } + } + } + } + + private func stopObservingAlertView() { + cancellable?.cancel() + } +} diff --git a/MVMCoreUI/Alerts/MVMCoreAlertDelegateProtocol.h b/MVMCoreUI/Alerts/MVMCoreAlertDelegateProtocol.h deleted file mode 100644 index 1b8d65c1..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertDelegateProtocol.h +++ /dev/null @@ -1,36 +0,0 @@ -// -// MVMCoreAlertDelegateProtocol.h -// mobilefirst -// -// Created by Pfeil, Scott Robert on 8/8/17. -// Copyright © 2017 Verizon Wireless. All rights reserved. -// -// Called for popup style alerts. - -#import -@class MVMCoreAlertObject; -@class MVMCoreLoadObject; -@class MVMCoreErrorObject; - -@protocol MVMCoreAlertDelegateProtocol - -@optional - -// helps tracking alert state -- (nullable NSDictionary *)additionalAlertDataToTrackForAlertWithObject:(nullable MVMCoreAlertObject *)alertObject; - -// All are performed on the main thread. -- (void)alertShown:(nonnull UIAlertController *)alertController; -- (void)alertCancelled:(nonnull UIAlertController *)alertController; -- (void)alertDismissed:(nonnull UIAlertController *)alertController; -- (void)alertPaused:(nonnull UIAlertController *)alertController; -- (void)alertUnpaused:(nonnull UIAlertController *)alertController; - -/** Get the alert object whose data will be presented. Overwrite this to alter how you want the alert to show. - * @param loadObject The load object. - * @param errorObject An error object if there was an error. - * @return Returns the alert object. - * Details: Gets the alert that will display to the screen. Easier to subclass here to avoid subclassing the displaying logic. */ -- (nullable MVMCoreAlertObject *)alertObjectToShow:(nonnull MVMCoreLoadObject *)loadObject error:(nullable MVMCoreErrorObject *)errorObject; - -@end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift b/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift deleted file mode 100644 index 6f52ce93..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// MVMCoreAlertHandler+Extension.swift -// MVMCore -// -// Created by Scott Pfeil on 9/15/20. -// Copyright © 2020 myverizon. All rights reserved. -// - -import Foundation - -public extension MVMCoreAlertHandler { - - /// Re-evaluates the queue operations - @objc func reevaluteQueue() { - var highestReadyOperation: MVMCoreTopAlertOperation? - var executingOperation: MVMCoreTopAlertOperation? - for case let operation as MVMCoreTopAlertOperation in topAlertQueue.operations { - guard !operation.isCancelled, - !operation.isFinished else { continue } - if operation.isReady, - highestReadyOperation == nil || operation.queuePriority.rawValue > highestReadyOperation!.queuePriority.rawValue { - highestReadyOperation = operation - } - if operation.isExecuting { - executingOperation = operation - } - } - guard let currentOperation = executingOperation else { return } - - // Cancel the executing operation if it is no longer ready to run. Re-add for later if it is persistent. - guard currentOperation.isReady else { - currentOperation.reAddAfterCancel = currentOperation.topAlertObject.persistent - currentOperation.cancel() - return - } - - // If the highest priority operation is not executing, and the executing operation is persistent, cancel it. - if let newOperation = highestReadyOperation, - currentOperation != newOperation, - currentOperation.topAlertObject.persistent { - currentOperation.reAddAfterCancel = true - currentOperation.cancel() - } - } - - /// Registers to know when pages change. - @objc func registerForPageChanges() { - MVMCoreNavigationHandler.shared()?.addDelegate(self) - } -} - -extension MVMCoreAlertHandler: MVMCorePresentationDelegateProtocol { - // Update displayable for each top alert operation when page type changes, in top queue priority order. - public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { - guard topAlertQueue.operations.count > 0 else { return } - let viewController = MVMCoreUIUtility.getViewControllerTraversingManagers(viewController) - guard viewController == MVMCoreUISplitViewController.main()?.getCurrentViewController() else { return } - let pageType = (viewController as? MVMCoreViewControllerProtocol)?.pageType - topAlertQueue.operations.compactMap { - $0 as? MVMCoreTopAlertOperation - }.sorted { - $0.queuePriority.rawValue > $1.queuePriority.rawValue - }.forEach { - $0.updateDisplayable(byPageType: pageType) - } - reevaluteQueue() - } -} - diff --git a/MVMCoreUI/Alerts/MVMCoreAlertHandler.h b/MVMCoreUI/Alerts/MVMCoreAlertHandler.h deleted file mode 100644 index 311b0c61..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertHandler.h +++ /dev/null @@ -1,119 +0,0 @@ -// -// MVMCoreAlertHandler.h -// myverizon -// -// Created by Scott Pfeil on 3/10/14. -// Copyright (c) 2014 Verizon Wireless. All rights reserved. -// -// Keeps track of alerts and handles them. Should always use this to present alerts in mf. - -#import -#import -#import -#import - -@class MVMCoreAlertObject; -@class MVMCoreTopAlertOperation; - -@interface MVMCoreAlertHandler : NSObject - -// An operation queue for displaying popup alerts. -@property (nonnull, strong, nonatomic) NSOperationQueue *popupAlertQueue; - -// An operation queue for top alerts -@property (nonnull, strong, nonatomic) NSOperationQueue *topAlertQueue; - -/// Returns the shared instance of this singleton -+ (nullable instancetype)sharedAlertHandler; - -#pragma mark - Popup Alert Functions - -/// Returns if any alert is currently showing (even if supressed). -- (BOOL)alertCurrentlyShowing; - -/// Returns if a greedy alert is currently showing (even if supressed). -- (BOOL)greedyAlertShowing; - -/** Shows the popup with the passed in parameter. - * @param title The title of the alert. - * @param message The message of the alert. - * @param actions An array of actions for the alert. - * @param isGreedy Sets up a greedy popup. In other words, any popups currently shown or queued are dismissed. - * @return Returns the UIAlertController. - */ -- (nonnull UIAlertController *)showAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nullable NSArray*)actions isGreedy:(BOOL)isGreedy; - -/** Shows the alert. - * @param title The title of the alert. - * @param message The message of the alert. - * @param actions An array of actions for the alert. - * @param alertStyle Popup or action sheet - * @param isGreedy Sets up a greedy alert. In other words, any alerts currently shown or queued are dismissed. - * @param alertDelegate The delegate to be notified. - * @return Returns the UIAlertController. - */ -- (nonnull UIAlertController *)showAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nullable NSArray*)actions alertStyle:(UIAlertControllerStyle)alertStyle isGreedy:(BOOL)isGreedy alertDelegate:(nullable NSObject *)alertDelegate; - -/** Shows the popup with the passed in alert object. This is a convenience method that automatically handles using the proper alert type based on what's available. - * @param alertObject The alert object to use for the alert. - * @return Returns UIAlertController. - */ -- (nonnull UIAlertController *)showAlertWithAlertObject:(nonnull MVMCoreAlertObject *)alertObject; - -/** Cancels and removes an alert operation for the given alertObject. - * @param alertObject The alertObject scheduled to be shown. - */ -- (void)removeAlertViewForObject:(nonnull MVMCoreAlertObject *)alertObject; - -/** Iterates through all scheduled alerts and cancels any that match the provided predicate. -* @param predicate The predicate block to decide whether to cancel an alert. -*/ -- (void)removeAlertViewUsingPredicate:(BOOL(^_Nonnull)(MVMCoreAlertObject * _Nonnull obj))predicate; - -/// Removes all alerts. -- (void)removeAllAlertViews; - -#pragma mark - Supression Functions - -/// Returns true if alerts are supressed. -- (BOOL)mfAlertsSupressed; - -/// Supresses the alerts (Used by other "apps" in our app). -- (void)supressMFAlerts; - -/// Unsupresses the alerts (Used by other "apps" in our app). -- (void)unSupressMFAlerts; - -#pragma mark - Top Alert Functions - -/// Show based on the object. Will be used by the architecture. Creates an operation and calls addTopAlertOperation. -- (void)showTopAlertWithObject:(nullable MVMCoreTopAlertObject *)topAlertObject; - -/// Adds the top alert operation to the queue. -- (void)addTopAlertOperation:(nonnull MVMCoreTopAlertOperation *)topAlertOperation; - -/// Convenience functions -- (void)showTopAlertErrorWithMessage:(nullable NSString *)message; -- (void)showTopAlertConfirmationWithMessage:(nullable NSString *)message; -- (void)showTopAlertWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; -- (void)showTopAlertWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler; - -/// Hides the current alert view. -- (void)hideTopAlertView; - -/// Hides a persistent alert based on the type string. -- (void)hidePersistentTopAlertViewOfType:(nullable NSString *)type; - -/// Hides a alert based on the type string. -- (void)hideTopAlertViewOfType:(nullable NSString *)type; - -/// Removes a scheduled top alert given its top alert object. -- (void)removeTopAlertForObject:(nonnull MVMCoreTopAlertObject *)topAlertObject; - -/// Removes all top alerts. -- (void)removeAllTopAlerts; - -/// Returns YES if the persistent type is already registered in the alert queue. -- (BOOL)hasPersistentTopAlertOfType:(nullable NSString *)type; - -@end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertHandler.m b/MVMCoreUI/Alerts/MVMCoreAlertHandler.m deleted file mode 100644 index 16fb1f4a..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertHandler.m +++ /dev/null @@ -1,274 +0,0 @@ -// -// MVMCoreAlertHandler.m -// myverizon -// -// Created by Scott Pfeil on 3/10/14. -// Copyright (c) 2014 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreAlertHandler.h" -#import "MVMCoreAlertObject.h" -@import MVMCore.MVMCoreAlertController; -#import "MVMCoreAlertOperation.h" -#import "MVMCoreTopAlertOperation.h" -@import MVMCore.MVMCoreJSONConstants; -@import MVMCore.NSDictionary_MFConvenience; -@import MVMCore.NSArray_MFConvenience; -#import - -@interface MVMCoreAlertHandler () - -// Flag that keeps track of if the alerts are supressed or not. -@property (assign, nonatomic) BOOL mfAlertsSupressed; - -@end - -@implementation MVMCoreAlertHandler - -+ (instancetype)sharedAlertHandler { - static dispatch_once_t once; - static id sharedInstance; - - dispatch_once(&once, ^{ - sharedInstance = [[self alloc] init]; - }); - - return sharedInstance; -} - -- (nullable instancetype)init { - if (self = [super init]) { - self.popupAlertQueue = [[NSOperationQueue alloc] init]; - self.popupAlertQueue.maxConcurrentOperationCount = 1; - self.topAlertQueue = [[NSOperationQueue alloc] init]; - self.topAlertQueue.maxConcurrentOperationCount = 1; - [self registerForPageChanges]; - } - return self; -} - -#pragma mark - Popup Alert Functions - -- (BOOL)alertCurrentlyShowing { - return (self.popupAlertQueue.operationCount > 0); -} - -- (BOOL)greedyAlertShowing { - if ([self alertCurrentlyShowing]) { - NSInteger index = [self.popupAlertQueue.operations indexOfObjectPassingTest:^BOOL(__kindof MVMCoreAlertOperation * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - if (obj.isExecuting && obj.isGreedy && stop) { - *stop = YES; - return YES; - } else { - return NO; - } - }]; - return (index != NSNotFound); - } - return NO; -} - -- (nonnull UIAlertController *)showAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nullable NSArray*)actions isGreedy:(BOOL)isGreedy { - return [self showAlertWithTitle:title message:message actions:actions isGreedy:isGreedy alertDelegate:nil]; -} - -- (nonnull UIAlertController *)showAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nullable NSArray*)actions isGreedy:(BOOL)isGreedy alertDelegate:(nullable NSObject *)alertDelegate { - return [self showAlertWithTitle:title message:message actions:actions alertStyle:UIAlertControllerStyleAlert isGreedy:isGreedy alertDelegate:alertDelegate]; -} - -- (nonnull UIAlertController *)showAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nullable NSArray*)actions alertStyle:(UIAlertControllerStyle)alertStyle isGreedy:(BOOL)isGreedy alertDelegate:(nullable NSObject *)alertDelegate { - - // It's a greedy alert! Clear all alerts that are queued up and the one that is showing - if (isGreedy) { - [self removeAllAlertViews]; - } - - if (alertStyle == UIAlertControllerStyleActionSheet && UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPhone) { - // ActionSheets are not supported on iPad interfaces without a source rect (i.e. a source element) which isn't currently supported for our generic handling. - alertStyle = UIAlertControllerStyleAlert; - } - - // Create the alert. Adds the actions one by one. - MVMCoreAlertController *alertController = [MVMCoreAlertController alertControllerWithTitle:(title ?: @"") message:message preferredStyle:alertStyle]; - for (NSUInteger i = 0; i < [actions count]; i++) { - UIAlertAction *action = [actions objectAtIndex:i ofType:[UIAlertAction class]]; - if (action) { - [alertController addAction:action]; - } - } - - MVMCoreAlertOperation *alertOperation = [[MVMCoreAlertOperation alloc] initWithAlert:alertController isGreedy:isGreedy alertDelegate:alertDelegate]; - [self.popupAlertQueue addOperation:alertOperation]; - return alertController; -} - -- (nonnull UIAlertController *)showAlertWithAlertObject:(nonnull MVMCoreAlertObject *)alertObject { - MVMCoreAlertController *controller = (MVMCoreAlertController *)[self showAlertWithTitle:alertObject.title message:alertObject.message actions:alertObject.actions alertStyle:alertObject.alertStyle isGreedy:alertObject.isGreedy alertDelegate:alertObject.alertDelegate]; - controller.alertObject = alertObject; - return controller; -} - -- (void)removeAlertViewForObject:(MVMCoreAlertObject *)alertObject { - for (MVMCoreAlertOperation *operation in self.popupAlertQueue.operations) { - if ([operation.currentAlertView isKindOfClass:[MVMCoreAlertController class]] && [(MVMCoreAlertController *)operation.currentAlertView alertObject] == alertObject) { - [operation cancel]; - } - } -} - -- (void)removeAlertViewUsingPredicate:(BOOL(^)(MVMCoreAlertObject *obj))predicate { - for (MVMCoreAlertOperation *operation in self.popupAlertQueue.operations) { - if ([operation.currentAlertView isKindOfClass:[MVMCoreAlertController class]]) { - MVMCoreAlertObject *alertObject = [(MVMCoreAlertController *)operation.currentAlertView alertObject]; - if (alertObject && predicate(alertObject)) { - [operation cancel]; - } - } - } -} - -- (void)removeAllAlertViews { - [self.popupAlertQueue cancelAllOperations]; -} - -#pragma mark - Supression Functions - -- (BOOL)mfAlertsSupressed { - return _mfAlertsSupressed; -} - -- (void)supressMFAlerts { - if (!self.mfAlertsSupressed) { - self.mfAlertsSupressed = YES; - for (MVMCoreAlertOperation *operation in self.popupAlertQueue.operations) { - [operation pause]; - } - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - [operation pause]; - } - } -} - -- (void)unSupressMFAlerts { - if (self.mfAlertsSupressed) { - self.mfAlertsSupressed = NO; - for (MVMCoreAlertOperation *operation in self.popupAlertQueue.operations) { - [operation unpause]; - } - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - [operation unpause]; - } - } -} - -#pragma mark - Top Alert Functions - -- (void)addTopAlertOperation:(nonnull MVMCoreTopAlertOperation *)topAlertOperation { - __block MVMCoreTopAlertOperation *alertOperation = topAlertOperation; - __weak typeof(self) weakSelf = self; - [alertOperation setCompletionBlock:^{ - - // If the alert was cancelled to show another with higher priority, re-add to the operation when cancelled to the queue. - if (alertOperation.reAddAfterCancel) { - MVMCoreTopAlertOperation *newOperation = [alertOperation copy]; - newOperation.reAddAfterCancel = NO; - [weakSelf addTopAlertOperation:newOperation]; - } - alertOperation = nil; - }]; - - NSString *currentPageType = ((UIViewController *)[[MVMCoreUISplitViewController mainSplitViewController] getCurrentDetailViewController]).pageType; - [alertOperation updateDisplayableByPageType:currentPageType]; - - [self.topAlertQueue addOperation:alertOperation]; - [self reevaluteQueue]; -} - -- (void)showTopAlertWithObject:(nullable MVMCoreTopAlertObject *)topAlertObject { - MVMCoreTopAlertOperation *alertOperation = [[MVMCoreTopAlertOperation alloc] initWithTopAlertObject:topAlertObject]; - [self addTopAlertOperation:alertOperation]; -} - -- (void)showTopAlertErrorWithMessage:(nullable NSString *)message { - - MVMCoreTopAlertObject *topAlertObject = [[MVMCoreTopAlertObject alloc] initWithType:ValueTypeError message:message]; - [self showTopAlertWithObject:topAlertObject]; -} - -- (void)showTopAlertConfirmationWithMessage:(nullable NSString *)message { - - MVMCoreTopAlertObject *topAlertObject = [[MVMCoreTopAlertObject alloc] initWithType:ValueTypeSuccess message:message]; - [self showTopAlertWithObject:topAlertObject]; -} - -- (void)showTopAlertWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - - MVMCoreTopAlertObject *topAlertObject = [[MVMCoreTopAlertObject alloc] initWithType:type message:message subMessage:subMessage persistent:persistent actionMap:actionMap additionalData:additionalData]; - [self showTopAlertWithObject:topAlertObject]; -} - -- (void)showTopAlertWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler { - - MVMCoreTopAlertObject *topAlertObject = [[MVMCoreTopAlertObject alloc] initWithType:type message:message subMessage:subMessage persistent:persistent buttonTitle:buttonTitle userActionHandler:userActionHandler]; - [self showTopAlertWithObject:topAlertObject]; -} - -- (void)hideTopAlertView { - - MVMCoreTopAlertOperation *currentOperation = [self.topAlertQueue.operations firstObject]; - currentOperation.topAlertObject.persistent = NO; - currentOperation.reAddAfterCancel = NO; - [currentOperation cancel]; -} - -- (BOOL)hasPersistentTopAlertOfType:(nullable NSString *)type { - BOOL hasAlert = NO; - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - if (operation.topAlertObject.persistent && [operation.topAlertObject.type isEqualToString:type]) { - hasAlert = YES; - } - } - return hasAlert; -} - -- (void)hidePersistentTopAlertViewOfType:(nullable NSString *)type { - if (type) { - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - - // Cancel all persistent operations of this type. - if (operation.topAlertObject.persistent && [operation.topAlertObject.type isEqualToString:type]) { - operation.reAddAfterCancel = NO; - [operation cancel]; - } - } - } -} - -- (void)hideTopAlertViewOfType:(nullable NSString *)type { - if (type) { - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - - // Cancel all operations of this type. - if ([operation.topAlertObject.type isEqualToString:type]) { - operation.reAddAfterCancel = NO; - [operation cancel]; - } - } - } -} - -- (void)removeTopAlertForObject:(MVMCoreTopAlertObject *)topAlertObject { - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - // Finds an cancels top alerts associated with the object. - if (operation.topAlertObject == topAlertObject) { - operation.reAddAfterCancel = NO; - [operation cancel]; - } - } -} - -- (void)removeAllTopAlerts { - [self.topAlertQueue cancelAllOperations]; -} - -@end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertObject+Swift.swift b/MVMCoreUI/Alerts/MVMCoreAlertObject+Swift.swift deleted file mode 100644 index 0adae2e2..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertObject+Swift.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// MVMCoreAlertObject+Swift.swift -// MVMCore -// -// Created by Suresh, Kamlesh on 7/10/20. -// Copyright © 2020 myverizon. All rights reserved. -// - - -public extension MVMCoreAlertObject { - - static func alertObject(from alertModel: AlertModel, actions: [UIAlertAction]? = nil, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> MVMCoreAlertObject? { - - let actionsForAlert = actions ?? generateActions(from: alertModel.alertActions, additionalData: additionalData, delegateObject: delegateObject) - - let alertObject = MVMCoreAlertObject(popupAlertWithTitle: alertModel.title, - message: alertModel.message, - actions: actionsForAlert, - isGreedy: false) - - alertObject?.alertStyle = alertModel.style - alertObject?.pageJson = alertModel.analyticsData - - return alertObject - } - - static func generateActions(from buttonModels: [AlertButtonModel], additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?, additionalHandling: ((AlertButtonModel, UIAlertAction)->())? = nil) -> [UIAlertAction] { - return buttonModels.map { alertButtonModel in - let alertAction = UIAlertAction(title: alertButtonModel.title, style: alertButtonModel.style) { action in - Task(priority: .userInitiated) { - do { - try await (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.performAction( - with: alertButtonModel.action, - additionalData: additionalData, - delegateObject: delegateObject - ) - } catch { - - } - additionalHandling?(alertButtonModel, action) - } - } - return alertAction - } - } - - @objc static func alertObjectWith(action actionJson: [AnyHashable: Any]?, additionalData: [AnyHashable: Any]?, delegateObject: DelegateObject?, error: AutoreleasingUnsafeMutablePointer?) -> MVMCoreAlertObject? { - - guard let alertJson = actionJson?.optionalDictionaryForKey("alert"), - (alertJson.optionalStringForKey(KeyTitle) != nil || alertJson.optionalStringForKey(KeyMessage) != nil), - let actionsList = alertJson.optionalArrayForKey("alertActions") as? [[AnyHashable: Any]] - else { - error?.pointee = MVMCoreErrorObject(title: nil, message: MVMCoreGetterUtility.hardcodedString(withKey: HardcodedErrorUnableToProcess), code: ErrorCode.popupFailed.rawValue, domain: ErrorDomainNative, location: String(describing: self)) - return nil - } - - var actionsForAlert: [UIAlertAction] = [] - - for actionJson in actionsList { - let style = UIAlertAction.Style(rawValue: actionJson.stringForkey("style")) - let alertAction = UIAlertAction(title: actionJson.optionalStringForKey(KeyTitle), style: style) { action in - MVMCoreActionHandler.shared()?.handleAction(with: actionJson.optionalDictionaryForKey("action"), - additionalData: additionalData, - delegateObject: delegateObject) - } - actionsForAlert.append(alertAction) - } - - let alertObject = MVMCoreAlertObject(popupAlertWithTitle: alertJson.optionalStringForKey(KeyTitle), - message: alertJson.optionalStringForKey(KeyMessage), - actions: actionsForAlert, - isGreedy: false) - - if let alertStyle = alertJson.optionalStringForKey("style") { - alertObject?.alertStyle = UIAlertController.Style(rawValue: alertStyle) - } - - if let analyticsData = alertJson.optionalDictionaryForKey("analyticsData") { - alertObject?.pageJson = ["analyticsData": analyticsData] - } - - return alertObject - } -} diff --git a/MVMCoreUI/Alerts/MVMCoreAlertObject.h b/MVMCoreUI/Alerts/MVMCoreAlertObject.h deleted file mode 100644 index a3a019b3..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertObject.h +++ /dev/null @@ -1,65 +0,0 @@ -// -// MVMCoreAlertObject.h -// myverizon -// -// Created by Scott Pfeil on 11/21/14. -// Copyright (c) 2014 Verizon Wireless. All rights reserved. -// -// An object for keeping track of all alert variables. Easier to pass around. - -#import -@import MVMCore.MVMCoreActionDelegateProtocol; -@import MVMCore.MVMCoreLoadDelegateProtocol; -@import MVMCore.MVMCorePresentationDelegateProtocol; -#import - -@class MVMCoreErrorObject; -@class MVMCoreLoadObject; -@class DelegateObject; - -typedef NS_ENUM(NSInteger, MFAlertType) { - MFAlertTypePopup = 0, - MFAlertTypeField, - MFAlertTypeTop, - MFAlertTypeNone -}; - -typedef void (^TextFieldErrorHandler)(NSArray * _Nonnull fieldErrors); - -@interface MVMCoreAlertObject : NSObject - -@property (nullable, strong, nonatomic) NSString *title; -@property (nullable, copy, nonatomic) NSDictionary *pageJson; -@property (nullable, strong, nonatomic) NSString *message; -@property (nonnull, strong, nonatomic) NSArray *actions; -@property (nonatomic) BOOL isGreedy; -@property (nonatomic) UIAlertControllerStyle alertStyle; -@property (nonatomic) MFAlertType type; -@property (nonatomic) BOOL defaultAction; - -@property (nonnull, strong, nonatomic) NSArray *fieldErrors; -@property (nullable, nonatomic, copy) TextFieldErrorHandler textFieldErrorHandler; - -// Set to be notified of popup style alert events. -@property (nonatomic, weak, nullable) NSObject *alertDelegate; - -// Creates an alert object for an error with the passed in load object response info -+ (nullable instancetype)alertObjectForLoadObject:(nullable MVMCoreLoadObject *)loadObject error:(nullable MVMCoreErrorObject *)error delegateObject:(nullable DelegateObject *)delegateObject; -+ (nullable instancetype)alertObjectForPageType:(nullable NSString *)pageType responseInfo:(nullable NSDictionary *)responseInfo additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject; - -// Initializes a popup style alert object. Look at the alert handler to see what each is used for. -- (nullable instancetype)initPopupAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nonnull NSArray *)actions isGreedy:(BOOL)isGreedy; - -// Initializes a popup style alert object using the error passed in. Message is formatted default style. By defualt uses the Okay button to just dismiss the error. -- (nullable instancetype)initPopupAlertWithError:(nullable MVMCoreErrorObject *)error isGreedy:(BOOL)isGreedy; - -// Same as above but no default actions. They are passed in. -- (nullable instancetype)initPopupAlertWithError:(nullable MVMCoreErrorObject *)error actions:(nonnull NSArray *)actions isGreedy:(BOOL)isGreedy; - -// Returns the alert object made with the page json. If there is not enough data to make, we will set error -+ (nullable instancetype)alertObjectWithPage:(nullable NSDictionary *)page isGreedy:(BOOL)isGreedy additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject error:(MVMCoreErrorObject *_Nullable *_Nullable)error; - -// Will show this alert in it's appropriate type style. -- (void)showAlert; - -@end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertObject.m b/MVMCoreUI/Alerts/MVMCoreAlertObject.m deleted file mode 100644 index b99a5870..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertObject.m +++ /dev/null @@ -1,197 +0,0 @@ -// -// MVMCoreAlertObject.m -// myverizon -// -// Created by Scott Pfeil on 11/21/14. -// Copyright (c) 2014 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreAlertObject.h" -#import "MVMCoreAlertHandler.h" -#import "MVMCoreTopAlertObject.h" -@import MVMCore.MVMCoreCache; -@import MVMCore.MVMCoreErrorConstants; -@import MVMCore.MVMCoreErrorObject; -@import MVMCore.MVMCoreLoadObject; -@import MVMCore.MVMCoreGetterUtility; -@import MVMCore.NSDictionary_MFConvenience; -@import MVMCore.MVMCoreHardcodedStringsConstants; -@import MVMCore.MVMCoreJSONConstants; -@import MVMCore.Swift; -#import - -@interface MVMCoreAlertObject () - -@property (strong, nonatomic) MVMCoreLoadObject *loadObject; -@property (nullable, strong, nonatomic) NSString *systemDomain; -@property (nullable, strong, nonatomic) MVMCoreTopAlertObject *topAlertObject; -@end - -@implementation MVMCoreAlertObject - -+ (nullable instancetype)alertObjectForLoadObject:(nullable MVMCoreLoadObject *)loadObject error:(nullable MVMCoreErrorObject *)error delegateObject:(nullable DelegateObject *)delegateObject { - - MVMCoreAlertObject *alert = nil; - if (!error || [ErrorDomainServer isEqualToString:error.domain]) { - alert = [MVMCoreAlertObject alertObjectForPageType:loadObject.pageType responseInfo:loadObject.responseInfoMap additionalData:loadObject.dataForPage delegateObject:delegateObject]; - } else { - alert = [[MVMCoreAlertObject alloc] initPopupAlertWithError:error isGreedy:NO]; - } - - // only if actions are empty, then go inside and set OK as default action - if (alert.type == MFAlertTypePopup && alert.actions.count == 0) { - alert.defaultAction = YES; - alert.actions = @[[UIAlertAction actionWithTitle:[MVMCoreGetterUtility hardcodedStringWithKey:HardcodedOK] style:UIAlertActionStyleDefault handler:nil]]; - } - return alert; -} - -+ (nullable instancetype)alertObjectForPageType:(nullable NSString *)pageType responseInfo:(nullable NSDictionary *)responseInfo additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject { - MVMCoreUIDelegateObject *alertDelegateObject = nil; - if ([delegateObject isKindOfClass:[MVMCoreUIDelegateObject class]]) { - alertDelegateObject = (MVMCoreUIDelegateObject *)delegateObject; - } - - __block MVMCoreAlertObject *alert = [[MVMCoreAlertObject alloc] init]; - alert.title = [responseInfo string:KeyErrorHeading] ?: [MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorTitle]; - alert.message = [responseInfo string:KeyUserMessage] ?: [MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorUnableToProcess]; - - NSString *messageStyle = [responseInfo stringForKey:KeyMessageStyle]; - if ([ValueTypeFieldErrors isEqualToString:[responseInfo string:KeyType]]) { - - // field errors. - alert.type = MFAlertTypeField; - alert.fieldErrors = [responseInfo array:ValueTypeFieldErrors]; - } else { - - // Check for top alert (persistent or regular). - if ([messageStyle isEqualToString:ValueMessageStyleTopPersistent] || [messageStyle isEqualToString:ValueMessageStyleTop]) { - - alert.topAlertObject = [[MVMCoreTopAlertObject alloc] initWithResponseInfo:responseInfo]; - alert.topAlertObject.delegate = alertDelegateObject.topAlertDelegate; - alert.topAlertObject.pageType = pageType; - alert.type = MFAlertTypeTop; - } else if ([messageStyle isEqualToString:ValueMessageStylePopup]) { - - // Perform a popup. - alert.type = MFAlertTypePopup; - alert.alertStyle = UIAlertControllerStyleAlert; - - // Check if we have a popup driven by page object (otherwise by default it will just use response info title message with an OK button). - NSString *pageTypeForPopup = [responseInfo stringForKey:@"popupPageType"]; - [[MVMCoreCache sharedCache] fetchJSONForPageType:pageTypeForPopup queue:nil waitUntilFinished:YES completionHandler:^(NSDictionary * _Nullable jsonDictionary) { - - MVMCoreErrorObject *error = nil; - MVMCoreAlertObject *popupAlert = [MVMCoreAlertObject alertObjectWithPage:jsonDictionary isGreedy:NO additionalData:additionalData delegateObject:delegateObject error:&error]; - if (error) { - - // Error, popup page not found for page type. - popupAlert = [[MVMCoreAlertObject alloc] initPopupAlertWithError:error isGreedy:NO]; - } - - if (popupAlert) { - alert = popupAlert; - } - }]; - } else if (messageStyle.length == 0 && pageType) { - - // No message style! - alert.type = MFAlertTypeNone; - } else { - - // Default to popup - alert.type = MFAlertTypePopup; - alert.alertStyle = UIAlertControllerStyleAlert; - } - } - alert.alertDelegate = alertDelegateObject.alertDelegate; - return alert; -} - -- (nullable instancetype)initPopupAlertWithTitle:(nullable NSString *)title message:(nullable NSString *)message actions:(nonnull NSArray *)actions isGreedy:(BOOL)isGreedy { - if (self = [super init]) { - self.title = title; - self.message = message; - self.actions = actions; - self.isGreedy = isGreedy; - self.type = MFAlertTypePopup; - self.alertStyle = UIAlertControllerStyleAlert; - } - return self; -} - -- (nullable instancetype)initPopupAlertWithError:(nullable MVMCoreErrorObject *)error isGreedy:(BOOL)isGreedy { - - if (self = [super init]) { - self.title = error.title; - self.message = [NSString stringWithFormat:@"%@ (%@)",error.messageToDisplay,[error stringErrorCode]]; - self.actions = @[[UIAlertAction actionWithTitle:[MVMCoreGetterUtility hardcodedStringWithKey:HardcodedOK] style:UIAlertActionStyleDefault handler:nil]]; - self.defaultAction = YES; - self.isGreedy = isGreedy; - self.type = MFAlertTypePopup; - self.alertStyle = UIAlertControllerStyleAlert; - } - return self; -} - -- (nullable instancetype)initPopupAlertWithError:(nullable MVMCoreErrorObject *)error actions:(nonnull NSArray *)actions isGreedy:(BOOL)isGreedy { - - if (self = [super init]) { - self.title = error.title; - self.message = [NSString stringWithFormat:@"%@ (%@)",error.messageToDisplay,[error stringErrorCode]]; - self.actions = actions; - self.isGreedy = isGreedy; - self.type = MFAlertTypePopup; - self.alertStyle = UIAlertControllerStyleAlert; - } - return self; -} - -+ (nullable instancetype)alertObjectWithPage:(nullable NSDictionary *)page isGreedy:(BOOL)isGreedy additionalData:(nullable NSDictionary *)additionalData delegateObject:(nullable DelegateObject *)delegateObject error:(MVMCoreErrorObject *_Nullable *_Nullable)error { - - MVMCoreAlertObject *alert = [[MVMCoreAlertObject alloc] init]; - alert.title = [page string:KeyTitle]; - alert.pageJson = page; - alert.message = [page string:KeyMessage]; - alert.isGreedy = isGreedy; - alert.type = MFAlertTypePopup; - alert.alertStyle = UIAlertControllerStyleAlert; - - NSArray *actions = [page array:KeyLinks]; - NSMutableArray *actionsForAlert = [NSMutableArray array]; - for (NSDictionary *actionMap in actions) { - [actionsForAlert addObject:[UIAlertAction actionWithTitle:[actionMap stringForKey:KeyTitle] style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) { - [[MVMCoreUIActionHandler sharedActionHandler] handleActionWithDictionary:actionMap additionalData:additionalData delegateObject:delegateObject]; - }]]; - } - alert.actions = actionsForAlert; - - if ((alert.title.length > 0 || alert.message.length > 0) && alert.actions.count > 0) { - return alert; - } else { - if (error) { - id delegate = [delegateObject isKindOfClass:[MVMCoreUIDelegateObject class]] ? ((MVMCoreUIDelegateObject *)delegateObject).alertDelegate : nil; - *error = [[MVMCoreErrorObject alloc] initWithTitle:nil message:[MVMCoreGetterUtility hardcodedStringWithKey:HardcodedErrorUnableToProcess] code:ErrorCodePopupFailed domain:ErrorDomainNative location:[NSString stringWithFormat:@"%@_Popup_pageType:%@",NSStringFromClass([delegate class]),[page stringForKey:KeyPageType]]]; - } - return nil; - } -} - -- (void)showAlert { - - switch (self.type) { - case MFAlertTypeField: - self.textFieldErrorHandler(self.fieldErrors); - break; - case MFAlertTypeTop: - [[MVMCoreAlertHandler sharedAlertHandler] showTopAlertWithObject:self.topAlertObject]; - break; - case MFAlertTypePopup: - [[MVMCoreAlertHandler sharedAlertHandler] showAlertWithAlertObject:self]; - break; - default: - break; - } -} - -@end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertOperation.h b/MVMCoreUI/Alerts/MVMCoreAlertOperation.h deleted file mode 100644 index 2347acd7..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertOperation.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// MVMCoreAlertOperation.h -// myverizon -// -// Created by Scott Pfeil on 9/28/15. -// Copyright © 2015 Verizon Wireless. All rights reserved. -// -// Operation for handling an alert. Should NOT be on the main queue. - -#import -#import -@import MVMCore.MVMCoreOperation; -#import - -@interface MVMCoreAlertOperation : MVMCoreOperation - -/// Alert controller to be displayed. -@property (nonnull, readonly) UIAlertController *currentAlertView; - -/// If this operation is temporarily paused. -@property (readonly, getter=isPaused) BOOL paused; - -/// If this alert is a greedy alert (See MVMCoreAlertHandler). -@property (readonly, getter=isGreedy) BOOL greedy; - -/// The alert delegate if needed. -@property (readonly, nullable, nonatomic, weak) NSObject *alertDelegate; - -/// Initializes the operation with the alert to display and if it is greedy or not. -- (nullable instancetype)initWithAlert:(nonnull UIAlertController *)alert isGreedy:(BOOL)isGreedy; -- (nullable instancetype)initWithAlert:(nonnull UIAlertController *)alert isGreedy:(BOOL)isGreedy alertDelegate:(nullable id )alertDelegate; - -/// Pauses the operation. Temporarily removes any alert. -- (void)pause; - -/// Unpauses the operation, resuming any alert. -- (void)unpause; - -@end diff --git a/MVMCoreUI/Alerts/MVMCoreAlertOperation.m b/MVMCoreUI/Alerts/MVMCoreAlertOperation.m deleted file mode 100644 index b39a3267..00000000 --- a/MVMCoreUI/Alerts/MVMCoreAlertOperation.m +++ /dev/null @@ -1,242 +0,0 @@ -// -// MVMCoreAlertOperation.m -// myverizon -// -// Created by Scott Pfeil on 9/28/15. -// Copyright © 2015 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreAlertOperation.h" -#import -@import MVMCore.MVMCoreAlertController; -@import MVMCore.MVMCoreNavigationHandler; - -@interface MVMCoreAlertOperation () { - __block BOOL _paused; - __block BOOL _displayed; -} - -@property (readwrite, getter=isPaused) BOOL paused; - -@property (readwrite, getter=isGreedy) BOOL greedy; - -@property (readwrite, getter=isDisplayed) BOOL displayed; - -@property (readwrite, nullable, nonatomic, weak) NSObject *alertDelegate; - -// The currently displayed alert view. -@property (nullable, strong, nonatomic) UIAlertController *currentAlertView; - -// A boolean to keep track of if we alreadys signed up to observe. -@property (assign, nonatomic) BOOL alertBeingObserved; - -// For thread safety -@property (strong, nonatomic) dispatch_queue_t pausedQueue; -@property (strong, nonatomic) dispatch_queue_t displayedQueue; - -// Dismisses the alert. -- (void)dismissAlertView; - -// Begins observing for when the alert is dismissed. -- (void)observeForCurrentAlertViewDismissal; - -// Stops observing for when the alert is dismissed. -- (void)stopObservingAlertView; - -@end - -@implementation MVMCoreAlertOperation - -// The context for kvo -static void * XXContext = &XXContext; - -- (instancetype)init { - - self = [super init]; - if (self) { - self.pausedQueue = dispatch_queue_create("paused", DISPATCH_QUEUE_CONCURRENT); - self.displayedQueue = dispatch_queue_create("displayed", DISPATCH_QUEUE_CONCURRENT); - } - return self; -} - -- (nullable instancetype)initWithAlert:(nonnull UIAlertController *)alert isGreedy:(BOOL)isGreedy { - - if (self = [self init]) { - self.currentAlertView = alert; - self.greedy = isGreedy; - } - return self; -} - -- (nullable instancetype)initWithAlert:(nonnull UIAlertController *)alert isGreedy:(BOOL)isGreedy alertDelegate:(nullable NSObject *)alertDelegate { - if (self = [self initWithAlert:alert isGreedy:isGreedy]) { - self.alertDelegate = alertDelegate; - } - return self; -} - -- (void)dealloc { - [self stopObservingAlertView]; -} - -- (BOOL)isPaused { - __block BOOL isPaused; - dispatch_sync(self.pausedQueue, ^{ - isPaused = self->_paused; - }); - return isPaused; -} - -- (void)setPaused:(BOOL)paused { - dispatch_barrier_async(self.pausedQueue, ^{ - self->_paused = paused; - }); -} - -- (BOOL)isDisplayed { - __block BOOL isDisplayed; - dispatch_sync(self.displayedQueue, ^{ - isDisplayed = self->_displayed; - }); - return isDisplayed; -} - -- (void)setDisplayed:(BOOL)displayed { - dispatch_barrier_async(self.displayedQueue, ^{ - self->_displayed = displayed; - }); -} - -- (void)main { - - // Always check for cancellation before launching the task. - if ([self checkAndHandleForCancellation]) { - return; - } - - // Display alert only if alerts aren't supressed. - if (![[MVMCoreAlertHandler sharedAlertHandler] mfAlertsSupressed] && self.currentAlertView) { - - // Observe for when it is removed. - [self observeForCurrentAlertViewDismissal]; - - // Adds the presentation to the animation queue. - [[MVMCoreNavigationHandler sharedNavigationHandler] presentViewController:self.currentAlertView animated:YES delegate:nil completionHandler:^{ - - // We finished but it was not displayed yet. It's possible that it was cancelled. Finish this task - if (!self.isDisplayed) { - [self markAsFinished]; - } else if (self.isCancelled) { - [self dismissAlertView]; - } - }]; - } -} - -- (void)cancel { - [super cancel]; - - // Notify delegate that the alert is cancelled. - if ([self.alertDelegate respondsToSelector:@selector(alertCancelled:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.alertDelegate alertCancelled:self.currentAlertView]; - }); - } - - [self dismissAlertView]; -} - -- (void)dismissAlertView { - - if (self.isDisplayed) { - - // Dismisses. - [[MVMCoreNavigationHandler sharedNavigationHandler] dismissViewController:self.currentAlertView animated:YES]; - } -} - -- (void)pause { - [self willChangeValueForKey:@"isPaused"]; - self.paused = YES; - [self didChangeValueForKey:@"isPaused"]; - - // Notify delegate of pause. - if ([self.alertDelegate respondsToSelector:@selector(alertPaused:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.alertDelegate alertPaused:self.currentAlertView]; - }); - } - - // Dismiss until unpaused. - [self dismissAlertView]; -} - -- (void)unpause { - [self willChangeValueForKey:@"isPaused"]; - self.paused = NO; - [self didChangeValueForKey:@"isPaused"]; - - // Notify delegate of unpause. - if ([self.alertDelegate respondsToSelector:@selector(alertUnpaused:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.alertDelegate alertUnpaused:self.currentAlertView]; - }); - } - - // Show alert... - if (self.currentAlertView) { - [self start]; - } -} - -#pragma mark - Observer Functions - -- (void)observeForCurrentAlertViewDismissal { - if (!self.alertBeingObserved && ![[MVMCoreAlertHandler sharedAlertHandler] mfAlertsSupressed] && self.currentAlertView && [self.currentAlertView isKindOfClass:[UIAlertController class]]) { - self.alertBeingObserved = YES; - [self.currentAlertView addObserver:self forKeyPath:@"visible" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:XXContext]; - } -} - -- (void)stopObservingAlertView { - if (self.alertBeingObserved) { - [self.currentAlertView removeObserver:self forKeyPath:@"visible" context:XXContext]; - self.alertBeingObserved = NO; - } -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - if (context == XXContext && [keyPath isEqualToString:@"visible"]) { - if (![object isVisible]) { - - self.displayed = NO; - - // Notify delegate that the alert is dismissed. - if ([self.alertDelegate respondsToSelector:@selector(alertDismissed:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.alertDelegate alertDismissed:self.currentAlertView]; - }); - } - - // Is visible was set to NO, meaning that the alertview is no longer visible. - if (!self.isPaused) { - [self stopObservingAlertView]; - self.currentAlertView = nil; - [self markAsFinished]; - } - } else { - - self.displayed = YES; - - // Notify delegate that the alert is shown. - if ([self.alertDelegate respondsToSelector:@selector(alertShown:)]) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.alertDelegate alertShown:self.currentAlertView]; - }); - } - } - } -} - -@end diff --git a/MVMCoreUI/Atomic/Actions/ActionAlertHandler.swift b/MVMCoreUI/Atomic/Actions/ActionAlertHandler.swift index f3004f71..c1febb95 100644 --- a/MVMCoreUI/Atomic/Actions/ActionAlertHandler.swift +++ b/MVMCoreUI/Atomic/Actions/ActionAlertHandler.swift @@ -10,29 +10,12 @@ import Foundation import MVMCore /// Shows an alert using the model. -open class ActionAlertHandler: MVMCoreJSONActionHandlerProtocol { +open class ActionAlertHandler: MVMCoreActionHandlerProtocol { required public init() {} - open func performAction(with JSON: [AnyHashable : Any], model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { - var error: MVMCoreErrorObject? = nil - guard let alertObject = MVMCoreAlertObject.alertObjectWith(action: JSON, additionalData: additionalData, delegateObject: delegateObject, error: &error) else { - throw MVMCoreError.errorObject(error!) - } - (delegateObject?.actionDelegate as? MVMCoreUIActionDelegateProtocol)?.willShowPopup(with: alertObject, alertJson: JSON) - _ = await MainActor.run { - MVMCoreAlertHandler.shared()?.showAlert(with: alertObject) - } - } - - open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { + public func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { guard let model = model as? ActionAlertModel else { return } - var error: MVMCoreErrorObject? = nil - guard let alertObject = MVMCoreAlertObject.alertObject(from: model.alert, additionalData: additionalData, delegateObject: delegateObject, error: &error) else { - throw MVMCoreError.errorObject(error!) - } - (delegateObject?.actionDelegate as? MVMCoreUIActionDelegateProtocol)?.willShowPopup(with: alertObject, alertJson: try MVMCoreActionHandler.convertActionToJSON(model)) - _ = await MainActor.run { - MVMCoreAlertHandler.shared()?.showAlert(with: alertObject) - } + let alertObject = AlertObject(alertModel: model.alert, alertDelegate: (delegateObject as? MVMCoreUIDelegateObject)?.alertDelegate) + _ = await AlertHandler.shared().queueAlertToShow(with: alertObject) } } diff --git a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift index 069ee74f..626bc203 100644 --- a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift +++ b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift @@ -9,11 +9,23 @@ import Foundation import MVMCore -/// Collapse the current top notification. +/// Notifications that conform are collapsable and can collapse. +public protocol CollapsableNotificationProtocol { + /// Collapses the notification. + @MainActor + func collapse() +} + +/// Collapses the current notification if it can collapse, otherwise dismisses it. open class ActionCollapseNotificationHandler: MVMCoreActionHandlerProtocol { required public init() {} open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { - CoreUIObject.sharedInstance()?.globalTopAlertDelegate?.getTopAlertView?().collapseNotification?() + guard let notification = await NotificationHandler.shared()?.getCurrentNotification() else { return } + guard let notification = notification.0 as? CollapsableNotificationProtocol else { + NotificationHandler.shared()?.hideNotification() + return + } + await notification.collapse() } } diff --git a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift index a2e1f279..6e34884e 100644 --- a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift +++ b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift @@ -9,15 +9,11 @@ import Foundation import MVMCore -/// Collapse the current top notification. +/// Dismiss the current notification. open class ActionDismissNotificationHandler: MVMCoreActionHandlerProtocol { required public init() {} open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { - await withCheckedContinuation { continuation in - CoreUIObject.sharedInstance()?.globalTopAlertDelegate?.getTopAlertView?().hideAlertView?(true, completionHandler: { finished in - continuation.resume() - }) ?? continuation.resume() - } + NotificationHandler.shared()?.hideNotification() } } diff --git a/MVMCoreUI/Atomic/Actions/ActionPopupHandler.swift b/MVMCoreUI/Atomic/Actions/ActionPopupHandler.swift deleted file mode 100644 index 554a000a..00000000 --- a/MVMCoreUI/Atomic/Actions/ActionPopupHandler.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ActionPopupHandler.swift -// MVMCoreUI -// -// Created by Scott Pfeil on 7/16/22. -// Copyright © 2022 Verizon Wireless. All rights reserved. -// - -import Foundation -import MVMCore - -/// Shows a popup alert by grabbing the content from a Page in the cache using the pageType. -open class ActionPopupHandler: MVMCoreActionHandlerProtocol { - required public init() {} - - open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { - guard let model = model as? ActionPopupModel else { return } - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - MVMCoreCache.shared()?.fetchJSON(forPageType: model.pageType, queue: nil, waitUntilFinished: true, completionHandler: { json in - var error: MVMCoreErrorObject? = nil - guard let alertObject = MVMCoreAlertObject(page: json, isGreedy: false, additionalData: additionalData, delegateObject: delegateObject, error: &error) else { - continuation.resume(throwing: MVMCoreError.errorObject(error!)) - return - } - (delegateObject?.actionDelegate as? MVMCoreUIActionDelegateProtocol)?.willShowPopup(with: alertObject, alertJson: json!) - Task { @MainActor in - MVMCoreAlertHandler.shared()?.showAlert(with: alertObject) - continuation.resume() - } - }) - } - } -} diff --git a/MVMCoreUI/Atomic/Actions/ActionPopupModel.swift b/MVMCoreUI/Atomic/Actions/ActionPopupModel.swift deleted file mode 100644 index cbade308..00000000 --- a/MVMCoreUI/Atomic/Actions/ActionPopupModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ActionPopupModel.swift -// MVMCore -// -// Created by Suresh, Kamlesh on 12/16/19. -// Copyright © 2019 myverizon. All rights reserved. -// - -import MVMCore - -public struct ActionPopupModel: ActionModelProtocol { - //-------------------------------------------------- - // MARK: - Properties - //-------------------------------------------------- - - public static var identifier: String = "popup" - public var actionType: String = ActionPopupModel.identifier - public var pageType: String - public var extraParameters: JSONValueDictionary? - public var analyticsData: JSONValueDictionary? - - //-------------------------------------------------- - // MARK: - Initializer - //-------------------------------------------------- - - public init(pageType: String, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { - self.pageType = pageType - self.extraParameters = extraParameters - self.analyticsData = analyticsData - } -} diff --git a/MVMCoreUI/Atomic/Actions/ActionTopAlertHandler.swift b/MVMCoreUI/Atomic/Actions/ActionTopAlertHandler.swift deleted file mode 100644 index 49ca8561..00000000 --- a/MVMCoreUI/Atomic/Actions/ActionTopAlertHandler.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// ActionTopAlertHandler.swift -// MVMCoreUI -// -// Created by Scott Pfeil on 7/18/22. -// Copyright © 2022 Verizon Wireless. All rights reserved. -// - -import Foundation -import MVMCore - -/// Creates and shows an alert using the ResponseInfo of a Page found in the cache. -open class ActionTopAlertHandler: MVMCoreActionHandlerProtocol { - required public init() {} - - open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { - guard let model = model as? ActionTopAlertModel else { return } - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - MVMCoreCache.shared()?.fetchJSON(forPageType: model.pageType, queue: nil, waitUntilFinished: true, completionHandler: { json in - guard let responseInfo = json?.optionalDictionaryForKey(KeyResponseInfo) else { - continuation.resume(throwing: ModelRegistry.Error.decoderOther(message: "Alert Page \(model.pageType) missing ResponseInfo")) - return - } - - var alertObject = MVMCoreAlertObject(forPageType: model.pageType, responseInfo: responseInfo, additionalData: additionalData, delegateObject: delegateObject) - if let object = alertObject, - let closure = (delegateObject?.actionDelegate as? MVMCoreUIActionDelegateProtocol)?.willShowTopAlert { - alertObject = closure(object, json!) - } - alertObject?.showAlert() - continuation.resume() - }) - } - } -} diff --git a/MVMCoreUI/Atomic/Actions/ActionTopAlertModel.swift b/MVMCoreUI/Atomic/Actions/ActionTopAlertModel.swift deleted file mode 100644 index 440e318c..00000000 --- a/MVMCoreUI/Atomic/Actions/ActionTopAlertModel.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ActionTopAlertModel.swift -// MVMCore -// -// Created by Suresh, Kamlesh on 12/16/19. -// Copyright © 2019 myverizon. All rights reserved. -// - -import Foundation - -public struct ActionTopAlertModel: ActionModelProtocol { - - public static var identifier: String = "topAlert" - public var actionType: String = ActionTopAlertModel.identifier - public var pageType: String - public var extraParameters: JSONValueDictionary? - public var analyticsData: JSONValueDictionary? - - public init(pageType: String, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { - self.pageType = pageType - self.extraParameters = extraParameters - self.analyticsData = analyticsData - } -} diff --git a/MVMCoreUI/Atomic/Actions/ActionTopNotificationHandler.swift b/MVMCoreUI/Atomic/Actions/ActionTopNotificationHandler.swift index 858c1d09..47db28cc 100644 --- a/MVMCoreUI/Atomic/Actions/ActionTopNotificationHandler.swift +++ b/MVMCoreUI/Atomic/Actions/ActionTopNotificationHandler.swift @@ -15,6 +15,6 @@ open class ActionTopNotificationHandler: MVMCoreActionHandlerProtocol { open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { guard let model = model as? ActionTopNotificationModel else { return } - await MVMCoreUITopAlertView.sharedGlobal()?.showTopAlert(with: model.topNotification) + try await NotificationHandler.shared()?.showNotification(for: model.topNotification, delegateObject: delegateObject as? MVMCoreUIDelegateObject) } } diff --git a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift index 9850e8d0..07d13230 100644 --- a/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift +++ b/MVMCoreUI/Atomic/Actions/ActionTopNotificationModel.swift @@ -13,11 +13,11 @@ public struct ActionTopNotificationModel: ActionModelProtocol { public static var identifier: String = "topNotification" public var actionType: String = ActionTopNotificationModel.identifier - public var topNotification: TopNotificationModel + public var topNotification: NotificationModel public var extraParameters: JSONValueDictionary? public var analyticsData: JSONValueDictionary? - public init(topNotification: TopNotificationModel, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { + public init(topNotification: NotificationModel, _ extraParameters: JSONValueDictionary? = nil, _ analyticsData: JSONValueDictionary? = nil) { self.topNotification = topNotification self.extraParameters = extraParameters self.analyticsData = analyticsData diff --git a/MVMCoreUI/Atomic/Actions/AlertModel.swift b/MVMCoreUI/Atomic/Actions/AlertModel.swift index 6825898c..8a36ab03 100644 --- a/MVMCoreUI/Atomic/Actions/AlertModel.swift +++ b/MVMCoreUI/Atomic/Actions/AlertModel.swift @@ -9,8 +9,7 @@ import UIKit import MVMCore - -public class AlertButtonModel: Codable { +public struct AlertButtonModel: Codable { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +17,7 @@ public class AlertButtonModel: Codable { public var title: String public var action: ActionModelProtocol public var style: UIAlertAction.Style = .default + public var preferred: Bool = false //-------------------------------------------------- // MARK: - Initializer @@ -37,13 +37,14 @@ public class AlertButtonModel: Codable { case title case action case style + case preferred } //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- - required public init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) title = try typeContainer.decode(String.self, forKey: .title) @@ -51,36 +52,61 @@ public class AlertButtonModel: Codable { self.style = UIAlertAction.Style(rawValue: style) } action = try typeContainer.decodeModel(codingKey: .action) + preferred = try typeContainer.decodeIfPresent(Bool.self, forKey: .preferred) ?? false } - open func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(style.rawValueString, forKey: .style) try container.encodeModel(action, forKey: .action) + try container.encodeIfPresent(preferred, forKey: .preferred) } } -public class AlertModel: Codable { +public struct AlertModel: Codable, Identifiable, AlertModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - - public var title: String - public var message: String - public var style: UIAlertController.Style = .alert - public var alertActions: [AlertButtonModel] + + public var title: String? + public var message: String? + public var preferredStyle: UIAlertController.Style = .alert + public var buttonModels: [AlertButtonModel] public var analyticsData: JSONValueDictionary? + public var id: String + public var delegateObject: DelegateObject? + + public var actions: [UIAlertAction] { + get { + buttonModels.map({ alertButtonModel in + return alertButtonModel.generateAction(delegateObject: delegateObject) + }) + } + } + + public var preferredActionIndex: Int? { + get { + buttonModels.firstIndex(where: { alertButtonModel in + return alertButtonModel.preferred + }) + } + } + + //-------------------------------------------------- - // MARK: - Properties + // MARK: - Init //-------------------------------------------------- - public init(title: String, message: String, alertActions: [AlertButtonModel], style: UIAlertController.Style = .alert) { + public init(title: String? = nil, message: String? = nil, buttonModels: [AlertButtonModel], style: UIAlertController.Style = .alert, delegateObject: DelegateObject? = nil, id: String = UUID().uuidString) { self.title = title self.message = message - self.alertActions = alertActions - self.style = style + self.buttonModels = buttonModels + self.preferredStyle = style + self.delegateObject = delegateObject + self.id = id } //-------------------------------------------------- @@ -93,30 +119,46 @@ public class AlertModel: Codable { case alertActions case style case analyticsData + case id } //-------------------------------------------------- // MARK: - Codec //-------------------------------------------------- - required public init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + delegateObject = try decoder.get() title = try typeContainer.decode(String.self, forKey: .title) message = try typeContainer.decode(String.self, forKey: .message) - alertActions = try typeContainer.decode([AlertButtonModel].self, forKey: .alertActions) + buttonModels = try typeContainer.decode([AlertButtonModel].self, forKey: .alertActions) analyticsData = try typeContainer.decodeIfPresent(JSONValueDictionary.self, forKey: .analyticsData) if let style = try typeContainer.decodeIfPresent(String.self, forKey: .style) { - self.style = UIAlertController.Style(rawValue: style) + self.preferredStyle = UIAlertController.Style(rawValue: style) } + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString } - open func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(message, forKey: .message) - try container.encode(alertActions, forKey: .alertActions) - try container.encode(style.rawValueString, forKey: .style) + try container.encodeIfPresent(buttonModels, forKey: .alertActions) + try container.encode(preferredStyle.rawValueString, forKey: .style) try container.encodeIfPresent(analyticsData, forKey: .analyticsData) + try container.encode(id, forKey: .id) + } +} + +public extension AlertButtonModel { + func generateAction(with additionalData: [AnyHashable: Any]? = nil, delegateObject: DelegateObject? = nil, additionalHandling: ((AlertButtonModel, UIAlertAction)->())? = nil) -> UIAlertAction { + let alertAction = UIAlertAction(title: title, style: style) { action in + Task(priority: .userInitiated) { + try? await (delegateObject?.actionDelegate as? ActionDelegateProtocol)?.performAction(with: self.action, additionalData: additionalData, delegateObject: delegateObject) + additionalHandling?(self, action) + } + } + return alertAction } } diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift index 8818107b..08afefde 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ButtonModel.swift @@ -18,6 +18,8 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat //-------------------------------------------------- //Making static property as class property so that subclasses can override getter function of the property open class var identifier: String { "button" } + public var id: String = UUID().uuidString + public var accessibilityIdentifier: String? public var accessibilityText: String? public var title: String @@ -174,6 +176,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -201,6 +204,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) title = try typeContainer.decode(String.self, forKey: .title) @@ -263,6 +267,7 @@ open class ButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGroupWat open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(title, forKey: .title) try container.encode(enabled, forKey: .enabled) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift index 71431d0f..3a5429e1 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/CaretLinkModel.swift @@ -16,6 +16,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea //-------------------------------------------------- public static var identifier: String = "caretLink" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var accessibilityIdentifier: String? public var title: String @@ -41,6 +42,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case backgroundColor case accessibilityIdentifier case title @@ -61,6 +63,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) @@ -94,6 +97,7 @@ public class CaretLinkModel: ButtonModelProtocol, MoleculeModelProtocol, Enablea public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(title, forKey: .title) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift index 560027f3..a61ec150 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/ImageButtonModel.swift @@ -14,8 +14,9 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro //-------------------------------------------------- public static var identifier: String = "imageButton" + public var id: String = UUID().uuidString + public var backgroundColor: Color? - public var image: ImageViewModel? public var accessibilityText: String? @@ -34,6 +35,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro } private enum CodingKeys: String, CodingKey { + case id case moleculeName case image case backgroundColor @@ -52,6 +54,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) image = try typeContainer.decodeIfPresent(ImageViewModel.self, forKey: .image) accessibilityText = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityText) @@ -77,6 +80,7 @@ open class ImageButtonModel: ButtonModelProtocol, MoleculeModelProtocol, FormGro public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(image, forKey: .image) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift index 38bcb491..62ba0422 100644 --- a/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Buttons/Link/LinkModel.swift @@ -15,6 +15,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode //-------------------------------------------------- public class var identifier: String { "link" } + public var id: String = UUID().uuidString public var backgroundColor: Color? public var accessibilityIdentifier: String? @@ -48,6 +49,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -91,6 +93,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) @@ -137,6 +140,7 @@ open class LinkModel: ButtonModelProtocol, MoleculeModelProtocol, EnableableMode public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(title, forKey: .title) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagModel.swift index 823d2478..40cb98b7 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagModel.swift @@ -11,11 +11,14 @@ import MVMCore @objcMembers public class TagModel: MoleculeModelProtocol { public static var identifier: String = "tag" + public var id: String = UUID().uuidString + public var label: LabelModel public var action: ActionModelProtocol? public var backgroundColor: Color? private enum CodingKeys: String, CodingKey { + case id case moleculeName case label case action @@ -38,6 +41,7 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString label = try typeContainer.decode(LabelModel.self, forKey: .label) action = try typeContainer.decodeModelIfPresent(codingKey: .action) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) @@ -45,6 +49,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(label, forKey: .label) try container.encodeModelIfPresent(action, forKey: .action) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagsModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagsModel.swift index 6330bb94..22ce1d9a 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagsModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/Tags/TagsModel.swift @@ -11,10 +11,13 @@ import MVMCore @objcMembers public class TagsModel: MoleculeModelProtocol { public static var identifier: String = "tags" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var tags: [TagModel] private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case tags @@ -30,12 +33,14 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString tags = try typeContainer.decode([TagModel].self, forKey: .tags) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(tags, forKey: .tags) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift index 172f304d..96bb8acf 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownEntryField.swift @@ -66,7 +66,7 @@ import MVMCore // MARK: - Setup //-------------------------------------------------- - @objc public override func setupFieldContainerContent(_ container: UIView) { + @objc open override func setupFieldContainerContent(_ container: UIView) { super.setupFieldContainerContent(container) container.addSubview(dropDownCaretView) @@ -79,7 +79,7 @@ import MVMCore dropDownCaretView.centerYAnchor.constraint(equalTo: container.centerYAnchor).isActive = true } - public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { + open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) self.additionalData = additionalData guard let model = model as? BaseDropdownEntryFieldModel else { return } @@ -87,7 +87,7 @@ import MVMCore dropDownCaretView.setOptional(with: model.caretView, delegateObject, additionalData) } - @objc public override func dismissFieldInput(_ sender: Any?) { + @objc open override func dismissFieldInput(_ sender: Any?) { if !textField.isFirstResponder { performDropdownAction() } diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift index 6c2131ac..e5f2e0b7 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/BaseDropdownFieldModel.swift @@ -14,7 +14,7 @@ public var caretView: CaretViewModel? public var action: ActionModelProtocol? - public override class var identifier: String { "" } + open override class var identifier: String { "" } //-------------------------------------------------- // MARK: - Keys @@ -26,6 +26,10 @@ case action } + public override init(with text: String) { + super.init(with: text) + } + open override func setDefaults() { super.setDefaults() enableClipboardActions = false @@ -42,7 +46,7 @@ action = try typeContainer.decodeModelIfPresent(codingKey: .action) } - 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.encode(moleculeName, forKey: .moleculeName) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift index 75d2c887..1c88ddab 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/Dropdown Fields/Item Dropdown/ItemDropdownEntryFieldModel.swift @@ -16,6 +16,12 @@ public var options: [String] = [] public var selectedIndex: Int? + public init(with options: [String], selectedIndex: Int? = nil) { + self.options = options + self.selectedIndex = selectedIndex + super.init(with: options.first ?? "") + } + //-------------------------------------------------- // MARK: - Validation //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift index 7b3eee03..6f58cf64 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/EntryFieldModel.swift @@ -16,6 +16,7 @@ import Foundation //-------------------------------------------------- public class var identifier: String { "" } + public var id: String = UUID().uuidString public var backgroundColor: Color? public var accessibilityIdentifier: String? @@ -63,6 +64,7 @@ import Foundation //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -138,6 +140,7 @@ import Foundation required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decodeIfPresent(String.self, forKey: .title) @@ -166,6 +169,7 @@ import Foundation public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) diff --git a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift index db3c28c4..910712d5 100644 --- a/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift +++ b/MVMCoreUI/Atomic/Atoms/FormFields/TextFields/MdnEntryField.swift @@ -144,7 +144,9 @@ import MVMCore picker.displayedPropertyKeys = ["phoneNumbers"] picker.predicateForEnablingContact = NSPredicate(format: "phoneNumbers.@count > 0") picker.predicateForSelectionOfProperty = NSPredicate(format: "key == 'phoneNumbers'") - MVMCoreNavigationHandler.shared()?.present(picker, animated: true) + Task(priority: .userInitiated) { + await NavigationHandler.shared().present(viewController: picker, animated: true) + } } //-------------------------------------------------- @@ -215,8 +217,12 @@ import MVMCore proprietorTextDelegate?.textFieldDidEndEditing?(textField) - if validateMDNTextField() && isNationalMDN { - textField.text = MVMCoreUIUtility.formatMdn(textField.text) + if validateMDNTextField() { + if isNationalMDN { + textField.text = MVMCoreUIUtility.formatMdn(textField.text) + } + // Validate the base input field along with triggering form field validation rules. + validateText() } } diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift index 7b6b36d8..604a8a8d 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/CheckboxModel.swift @@ -20,6 +20,7 @@ //-------------------------------------------------- public static var identifier: String = "checkbox" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var accessibilityIdentifier: String? public var selected: Bool = false @@ -51,6 +52,7 @@ //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case accessibilityIdentifier case checked @@ -107,6 +109,8 @@ required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) if let borderWidth = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .borderWidth) { @@ -180,6 +184,7 @@ public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(groupName, forKey: .groupName) try container.encodeIfPresent(fieldKey, forKey: .fieldKey) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift index 4be1343a..cb301e22 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/HeartModel.swift @@ -14,6 +14,8 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- public static var identifier: String = "heart" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var accessibilityIdentifier: String? public var isActive: Bool = false @@ -27,6 +29,7 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -49,6 +52,8 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let isActive = try typeContainer.decodeIfPresent(Bool.self, forKey: .isActive) { self.isActive = isActive } @@ -75,6 +80,7 @@ open class HeartModel: MoleculeModelProtocol, EnableableModelProtocol { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) + try container.encode(id, forKey: .id) 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/RadioBoxModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift index 27defd48..acec6e83 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxModel.swift @@ -13,6 +13,8 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "radioBox" + public var id: String = UUID().uuidString + public var text: String public var subText: String? public var backgroundColor: Color? @@ -30,6 +32,7 @@ import MVMCore //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case text case subText @@ -58,6 +61,8 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString text = try typeContainer.decode(String.self, forKey: .text) subText = try typeContainer.decodeIfPresent(String.self, forKey: .subText) selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor) @@ -81,6 +86,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(text, forKey: .text) try container.encodeIfPresent(subText, forKey: .subText) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift index 6348eac5..f1c652f7 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioBoxesModel.swift @@ -13,6 +13,8 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "radioBoxes" + public var id: String = UUID().uuidString + public var boxes: [RadioBoxModel] public var backgroundColor: Color? public var accessibilityIdentifier: String? @@ -49,6 +51,7 @@ import MVMCore //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case enabled case readOnly @@ -75,6 +78,7 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString selectedAccentColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedAccentColor) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) @@ -91,6 +95,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(boxes, forKey: .boxes) try container.encodeIfPresent(selectedAccentColor, forKey: .selectedAccentColor) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift index ddd155be..6966f3e6 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButton.swift @@ -26,7 +26,7 @@ import VDSFormControlsTokens } } public var enabledColor: UIColor { - return radioModel?.inverted ?? false ? VDSFormControlsColor.borderOndark : VDSFormControlsColor.borderOnlight + return radioModel?.inverted ?? false ? VDSColor.elementsPrimaryOndark : VDSColor.elementsPrimaryOnlight } public var disabledColor: UIColor { return radioModel?.inverted ?? false ? VDSColor.interactiveDisabledOndark : VDSColor.interactiveDisabledOnlight diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift index 86eb36e9..99b5cdb7 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioButtonModel.swift @@ -15,6 +15,8 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- public static var identifier: String = "radioButton" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var accessibilityIdentifier: String? public var state: Bool = false @@ -35,6 +37,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -80,6 +83,8 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { self.state = state } @@ -105,6 +110,7 @@ open class RadioButtonModel: MoleculeModelProtocol, FormFieldProtocol { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) + try container.encode(id, forKey: .id) 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/RadioSwatchModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift index f0a194e9..91f13b48 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchModel.swift @@ -13,6 +13,8 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "radioSwatch" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var accessibilityIdentifier: String? public var color: Color = Color(uiColor: .mvmBlue) @@ -29,6 +31,7 @@ import MVMCore //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -54,6 +57,7 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) @@ -79,6 +83,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift index d5d3992a..f424593b 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/RadioSwatchesModel.swift @@ -13,6 +13,8 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "radioSwatches" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var accessibilityIdentifier: String? public var swatches: [RadioSwatchModel] @@ -43,6 +45,7 @@ import MVMCore //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -67,6 +70,7 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) swatches = try typeContainer.decode([RadioSwatchModel].self, forKey: .swatches) @@ -81,6 +85,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) diff --git a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift index e8d50851..429a5769 100644 --- a/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Selectors/ToggleModel.swift @@ -13,6 +13,8 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- public static var identifier: String = "toggle" + public var id: String = UUID().uuidString + public var accessibilityIdentifier: String? public var backgroundColor: Color? public var selected: Bool = false @@ -36,6 +38,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case state case animated @@ -86,6 +89,8 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let state = try typeContainer.decodeIfPresent(Bool.self, forKey: .state) { self.selected = state } @@ -128,6 +133,7 @@ public class ToggleModel: MoleculeModelProtocol, FormFieldProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encodeModelIfPresent(action, forKey: .action) diff --git a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift index 388f1669..7697143e 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ArrowModel.swift @@ -17,8 +17,9 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { public static var identifier: String { return "arrow" } - public var moleculeName: String? + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var disabledColor: Color = Color(uiColor: .mvmCoolGray3) public var color: Color = Color(uiColor: .mvmBlack) @@ -57,6 +58,7 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case disabledColor @@ -75,7 +77,7 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) - + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) if let disabledColor = try typeContainer.decodeIfPresent(Color.self, forKey: .disabledColor) { @@ -113,6 +115,7 @@ open class ArrowModel: MoleculeModelProtocol, EnableableModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(disabledColor, forKey: .disabledColor) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CaretViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CaretViewModel.swift index 1e2595ec..380328d5 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CaretViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CaretViewModel.swift @@ -15,6 +15,7 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "caretView" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var strokeColor: Color = Color(uiColor: .mvmBlack) public var strokeColor_inverted: Color = Color(uiColor: .mvmWhite) @@ -28,6 +29,7 @@ import MVMCore //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case strokeColor @@ -51,6 +53,8 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let strokeColor = try typeContainer.decodeIfPresent(Color.self, forKey: .strokeColor) { self.strokeColor = strokeColor } @@ -78,6 +82,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(strokeColor, forKey: .strokeColor) try container.encode(strokeColor_inverted, forKey: .strokeColor_inverted) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift index 164bb306..f5c0adeb 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CarouselIndicator/CarouselIndicatorModel.swift @@ -18,6 +18,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro return "" } + public var id: String = UUID().uuidString public var backgroundColor: Color? public var moleculeName: String? public var numberOfPages: Int = 0 @@ -44,6 +45,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case currentIndex @@ -66,6 +68,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString moleculeName = try typeContainer.decodeIfPresent(String.self, forKey: .moleculeName) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) @@ -112,6 +115,7 @@ open class CarouselIndicatorModel: CarouselPagingModelProtocol, MoleculeModelPro public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(numberOfPages, forKey: .numberOfPages) diff --git a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift index 58f398b6..ca0acd79 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/CheckboxLabelModel.swift @@ -18,6 +18,8 @@ public enum CheckboxPosition: String, Codable { @objcMembers open class CheckboxLabelModel: MoleculeModelProtocol { open class var identifier: String { "checkboxLabel" } public var moleculeName: String = CheckboxLabelModel.identifier + @DecodableDefault.UUIDString public var id: String + public var backgroundColor: Color? public var checkboxAlignment: CheckboxPosition? public var checkbox: CheckboxModel diff --git a/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift index 95cc40fd..8e533fde 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/DashLineModel.swift @@ -15,6 +15,8 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "dashLine" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var accessibilityIdentifier: String? public var dashColor: Color = Color(uiColor: .mvmCoolGray3) @@ -35,6 +37,7 @@ import MVMCore //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case accessibilityIdentifier @@ -56,6 +59,8 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let dashColor = try typeContainer.decodeIfPresent(Color.self, forKey: .dashColor) { self.dashColor = dashColor } @@ -74,6 +79,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(dashColor, forKey: .dashColor) try container.encode(isHidden, forKey: .isHidden) diff --git a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift index 03c18491..5079fc93 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ImageViewModel.swift @@ -13,6 +13,7 @@ //-------------------------------------------------- open class var identifier: String { "image" } + @DecodableDefault.UUIDString public var id: String public var backgroundColor: Color? public var moleculeName: String = ImageViewModel.identifier @@ -26,6 +27,7 @@ public var localBundle: Bundle? public var cornerRadius: CGFloat? public var clipsImage: Bool? + public var allowServerParameters: Bool? public var shouldMaskRecordedView: Bool? = false //-------------------------------------------------- @@ -44,6 +46,7 @@ //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case image @@ -56,5 +59,6 @@ case cornerRadius case clipsImage case shouldMaskRecordedView + case allowServerParameters } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift index 142bc700..01614f31 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/Label.swift @@ -276,6 +276,9 @@ public typealias ActionBlock = () -> () guard let labelModel = model as? LabelModel else { return } text = labelModel.text + if let accessibilityTraits = labelModel.accessibilityTraits { + self.accessibilityTraits = accessibilityTraits + } resetAttributeStyle() @@ -754,6 +757,37 @@ public typealias ActionBlock = () -> () return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) } + + /** + Provides a text container and layout manager of how the text would appear on screen. + They are used in tandem to derive low-level TextKit results of the label. + */ + public func abstractTextContainer() -> (NSTextContainer, NSLayoutManager, NSTextStorage)? { + + // Must configure the attributed string to translate what would appear on screen to accurately analyze. + guard let attributedText = attributedText else { return nil } + + let paragraph = NSMutableParagraphStyle() + paragraph.alignment = textAlignment + + let stagedAttributedString = NSMutableAttributedString(attributedString: attributedText) + stagedAttributedString.addAttributes([NSAttributedString.Key.paragraphStyle: paragraph], range: NSRange(location: 0, length: attributedText.string.count)) + + let textStorage = NSTextStorage(attributedString: stagedAttributedString) + let layoutManager = NSLayoutManager() + let textContainer = NSTextContainer(size: .zero) + + layoutManager.addTextContainer(textContainer) + textStorage.addLayoutManager(layoutManager) + + textContainer.lineFragmentPadding = 0.0 + textContainer.lineBreakMode = lineBreakMode + textContainer.maximumNumberOfLines = numberOfLines + textContainer.size = bounds.size + + return (textContainer, layoutManager, textStorage) + } + } // MARK: - Atomization diff --git a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift index d1995d19..19284756 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Label/LabelModel.swift @@ -7,13 +7,14 @@ // -@objcMembers open class LabelModel: MoleculeModelProtocol, Identifiable { +@objcMembers open class LabelModel: MoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- open class var identifier: String { "label" } public var id: String + public var backgroundColor: Color? public var text: String public var accessibilityText: String? @@ -28,14 +29,15 @@ public var makeWholeViewClickable: Bool? public var numberOfLines: Int? public var shouldMaskRecordedView: Bool? = false + public var accessibilityTraits: UIAccessibilityTraits? //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { - case moleculeName case id + case moleculeName case text case accessibilityText case textColor @@ -50,6 +52,7 @@ case makeWholeViewClickable case numberOfLines case shouldMaskRecordedView + case accessibilityTraits } enum AttributeTypeKey: String, CodingKey { @@ -60,9 +63,10 @@ // MARK: - Initializer //-------------------------------------------------- - public init(id: String = UUID().uuidString, text: String) { + public init(id: String = UUID().uuidString, text: String, textColor: Color? = nil) { self.id = id self.text = text + self.textColor = textColor } //-------------------------------------------------- @@ -96,6 +100,7 @@ makeWholeViewClickable = try typeContainer.decodeIfPresent(Bool.self, forKey: .makeWholeViewClickable) numberOfLines = try typeContainer.decodeIfPresent(Int.self, forKey: .numberOfLines) shouldMaskRecordedView = try typeContainer.decodeIfPresent(Bool.self, forKey: .shouldMaskRecordedView) ?? false + accessibilityTraits = try typeContainer.decodeIfPresent(UIAccessibilityTraits.self, forKey: .accessibilityTraits) // Later make protocol based validate outside of decoding? if let attributes = attributes { @@ -106,7 +111,7 @@ open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(moleculeName, forKey: .moleculeName) - try container.encodeIfPresent(id, forKey: .id) + try container.encode(id, forKey: .id) try container.encode(text, forKey: .text) try container.encodeIfPresent(accessibilityText, forKey: .accessibilityText) try container.encodeIfPresent(textColor, forKey: .textColor) @@ -121,5 +126,6 @@ try container.encodeIfPresent(makeWholeViewClickable, forKey: .makeWholeViewClickable) try container.encodeIfPresent(numberOfLines, forKey: .numberOfLines) try container.encodeIfPresent(shouldMaskRecordedView, forKey: .shouldMaskRecordedView) + try container.encodeIfPresent(accessibilityTraits, forKey: .accessibilityTraits) } } diff --git a/MVMCoreUI/Atomic/Atoms/Views/LeftRightLabelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LeftRightLabelModel.swift index 07c534f8..2f442dc8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LeftRightLabelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LeftRightLabelModel.swift @@ -15,6 +15,8 @@ import UIKit public static var identifier: String = "leftRightLabelView" public var moleculeName: String = LeftRightLabelModel.identifier + @DecodableDefault.UUIDString public var id: String + public var backgroundColor: Color? public var leftText: LabelModel public var rightText: LabelModel? diff --git a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift index cc9e7333..3b4c57e9 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LineModel.swift @@ -53,6 +53,8 @@ import VDSColorTokens //-------------------------------------------------- public static var identifier: String = "line" + public var id: String = UUID().uuidString + public var type: Style = .secondary public var frequency: Frequency? = .allExceptTop @@ -120,6 +122,7 @@ import VDSColorTokens //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case type case backgroundColor @@ -138,6 +141,8 @@ import VDSColorTokens required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let type = try typeContainer.decodeIfPresent(Style.self, forKey: .type) { self.type = type } @@ -158,6 +163,7 @@ import VDSColorTokens public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(id, forKey: .id) try container.encode(type, forKey: .type) try container.encode(inverted, forKey: .inverted) try container.encodeIfPresent(frequency, forKey: .frequency) diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift index f3d381bc..780f8829 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadImageView.swift @@ -289,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, height: height, customFallbackImage: imageModel.fallbackImage, localBundle: imageModel.localBundle) + loadImage(withName: imageModel.image, format: imageModel.imageFormat, width: width, height: height, customFallbackImage: imageModel.fallbackImage, allowServerParameters: imageModel.allowServerParameters ?? false, localBundle: imageModel.localBundle) } if let contentMode = imageModel.contentMode { diff --git a/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift b/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift index ddfba326..b74807fb 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/LoadingSpinnerModel.swift @@ -13,9 +13,10 @@ open class LoadingSpinnerModel: MoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - - public var backgroundColor: Color? public static var identifier: String = "loadingSpinner" + public var id: String = UUID().uuidString + + public var backgroundColor: Color? public var strokeColor = Color(uiColor: .mvmBlack) public var lineWidth: CGFloat = 4 public var diameter: CGFloat = 40 @@ -25,6 +26,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case strokeColor @@ -45,6 +47,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) if let diameter = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .diameter) { @@ -62,6 +65,7 @@ open class LoadingSpinnerModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(diameter, forKey: .diameter) diff --git a/MVMCoreUI/Atomic/Atoms/Views/MultiProgressModel.swift b/MVMCoreUI/Atomic/Atoms/Views/MultiProgressModel.swift index fb70ffd0..d664afa6 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/MultiProgressModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/MultiProgressModel.swift @@ -19,13 +19,17 @@ import Foundation } @objcMembers public class MultiProgressBarModel: MoleculeModelProtocol { + public static var identifier: String = "multiProgressBar" + public var id: String = UUID().uuidString + public var progressList: [SingleProgressBarModel] public var backgroundColor: Color? public var thickness: CGFloat? public var roundedCorners: Bool? private enum CodingKeys: String, CodingKey { + case id case moleculeName case progressList case thickness @@ -39,6 +43,7 @@ import Foundation required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString progressList = try typeContainer.decode([SingleProgressBarModel].self, forKey: .progressList) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) thickness = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .thickness) @@ -47,6 +52,7 @@ import Foundation public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(progressList, forKey: .progressList) try container.encodeIfPresent(thickness, forKey: .thickness) diff --git a/MVMCoreUI/Atomic/Atoms/Views/ProgressBarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/ProgressBarModel.swift index 7cef8936..439fad3d 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/ProgressBarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/ProgressBarModel.swift @@ -10,6 +10,8 @@ import Foundation @objcMembers public class ProgressBarModel: MoleculeModelProtocol { public static var identifier: String = "progressBar" + public var id: String = UUID().uuidString + @Percent public var percent: CGFloat public var color: Color = Color(uiColor: .mfCerulean()) public var backgroundColor: Color? = Color(uiColor: .mfLightSilver()) @@ -17,6 +19,7 @@ import Foundation public var thickness: CGFloat? private enum CodingKeys: String, CodingKey { + case id case moleculeName case roundedCorners case thickness @@ -31,6 +34,7 @@ import Foundation required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString percent = try typeContainer.decode(CGFloat.self, forKey: .percent) if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) { self.color = color @@ -44,6 +48,7 @@ import Foundation public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(percent, forKey: .percent) try container.encode(color, forKey: .color) diff --git a/MVMCoreUI/Atomic/Atoms/Views/StarModel.swift b/MVMCoreUI/Atomic/Atoms/Views/StarModel.swift index 56f4f7ad..7b427cde 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/StarModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/StarModel.swift @@ -13,6 +13,8 @@ open class StarModel: MoleculeModelProtocol { // MARK: - Properties //-------------------------------------------------- public static var identifier: String = "star" + public var id: String = UUID().uuidString + public var backgroundColor: Color? @Percent public var percent: CGFloat = 0 public var borderColor: Color? @@ -23,6 +25,7 @@ open class StarModel: MoleculeModelProtocol { // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case percent @@ -43,6 +46,7 @@ open class StarModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString if let percent = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .percent) { self.percent = percent } @@ -56,6 +60,7 @@ open class StarModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(percent, forKey: .percent) diff --git a/MVMCoreUI/Atomic/Atoms/Views/StarsModel.swift b/MVMCoreUI/Atomic/Atoms/Views/StarsModel.swift index f79f638e..10e6400a 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/StarsModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/StarsModel.swift @@ -13,6 +13,8 @@ import MVMCore // MARK: - Properties //-------------------------------------------------- public static var identifier: String = "stars" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var starBackgroundColor: Color? public var stars: [StarModel] @@ -25,6 +27,7 @@ import MVMCore // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case starBackgroundColor @@ -48,6 +51,8 @@ import MVMCore //-------------------------------------------------- required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString stars = try typeContainer.decode([StarModel].self, forKey: .stars) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) starBackgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .starBackgroundColor) @@ -63,6 +68,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(stars, forKey: .stars) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift index c87e091c..41d68493 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Tilelet.swift @@ -37,8 +37,11 @@ open class Tilelet: VDS.Tilelet, VDSMoleculeViewProtocol{ padding = viewModel.padding aspectRatio = viewModel.aspectRatio width = viewModel.width - textWidth = viewModel.textWidth - textPercentage = viewModel.textPercentage + if let value = viewModel.textWidth { + textWidth = .value(value) + } else if let percentage = viewModel.textPercentage { + textWidth = .percentage(percentage) + } titleModel = viewModel.titleModel(delegateObject: delegateObject, additionalData: additionalData) subTitleModel = viewModel.subTitleModel(delegateObject: delegateObject, additionalData: additionalData) badgeModel = viewModel.badge diff --git a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift index c6685364..b41d9eef 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/TileletModel.swift @@ -15,6 +15,7 @@ open class TileletModel: MoleculeModelProtocol { // MARK: - Properties //-------------------------------------------------- public static var identifier: String = "tilelet" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var color: TileContainer.BackgroundColor public var padding: TileContainer.Padding @@ -30,6 +31,7 @@ open class TileletModel: MoleculeModelProtocol { public var action: ActionModelProtocol? private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case color @@ -47,6 +49,7 @@ open class TileletModel: MoleculeModelProtocol { } required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString self.backgroundColor = try container.decodeIfPresent(Color.self, forKey: .backgroundColor) self.color = try container.decodeIfPresent(TileContainer.BackgroundColor.self, forKey: .color) ?? TileContainer.BackgroundColor.black self.padding = try container.decodeIfPresent(TileContainer.Padding.self, forKey: .padding) ?? TileContainer.Padding.padding4X @@ -65,9 +68,9 @@ open class TileletModel: MoleculeModelProtocol { public func titleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.TitleModel? { guard let title else { return nil } let attrs = title.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) - let style: Tilelet.TitleModel.TextStyle? = title.fontStyle?.vdsSubsetStyle() - if let style { - return .init(text: title.text, textAttributes: attrs, textStyle: style) + let style: TextStyle? = title.fontStyle?.vdsTextStyle() + if let style, let standardStyle = Tilelet.TitleModel.StandardStyle(rawValue: style.toStandardStyle().rawValue) { + return .init(text: title.text, textAttributes: attrs, standardStyle: standardStyle) } else { return .init(text: title.text, textAttributes: attrs) } @@ -75,16 +78,18 @@ open class TileletModel: MoleculeModelProtocol { public func subTitleModel(delegateObject: MVMCoreUIDelegateObject?, additionalData: [AnyHashable: Any]?) -> Tilelet.SubTitleModel? { guard let subTitle else { return nil } - let style: Tilelet.SubTitleModel.TextStyle? = subTitle.fontStyle?.vdsSubsetStyle() - if let style { - return .init(text: subTitle.text, textStyle: style) + let attrs = subTitle.attributes?.toVDSLabelAttributeModel(delegateObject: delegateObject, additionalData: additionalData) + let style: TextStyle? = subTitle.fontStyle?.vdsTextStyle() + if let style, let standardStyle = Tilelet.SubTitleModel.StandardStyle(rawValue: style.toStandardStyle().rawValue) { + return .init(text: subTitle.text, textAttributes: attrs, standardStyle: standardStyle) } else { - return .init(text: subTitle.text) + return .init(text: subTitle.text, textAttributes: attrs) } } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(color, forKey: .color) diff --git a/MVMCoreUI/Atomic/Atoms/Views/Video/VideoModel.swift b/MVMCoreUI/Atomic/Atoms/Views/Video/VideoModel.swift index 7c348760..27567926 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/Video/VideoModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/Video/VideoModel.swift @@ -10,6 +10,7 @@ import Foundation open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer { public static var identifier = "video" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var video: String public var showControls = false @@ -43,6 +44,7 @@ open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer { private var resignActiveListener: Any? private enum CodingKeys: String, CodingKey { + case id case moleculeName case video case showControls @@ -57,6 +59,7 @@ open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString video = try typeContainer.decode(String.self, forKey:.video) if let showControls = try typeContainer.decodeIfPresent(Bool.self, forKey: .showControls) { self.showControls = showControls @@ -72,6 +75,7 @@ open class VideoModel: MoleculeModelProtocol, PageBehaviorProtocolRequirer { open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(video, forKey: .video) try container.encode(showControls, forKey: .showControls) diff --git a/MVMCoreUI/Atomic/Atoms/Views/WebViewModel.swift b/MVMCoreUI/Atomic/Atoms/Views/WebViewModel.swift index 4cb2bd55..28d619a8 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/WebViewModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/WebViewModel.swift @@ -12,6 +12,8 @@ import MVMCore @objcMembers public class WebViewModel: MoleculeModelProtocol { public static var identifier: String = "webview" public var moleculeName: String = WebViewModel.identifier + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var url: URL? public var htmlString: String? @@ -23,6 +25,7 @@ import MVMCore } private enum CodingKeys: String, CodingKey{ + case id case moleculeName case backgroundColor case url @@ -39,6 +42,7 @@ import MVMCore required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) url = try typeContainer.decodeIfPresent(URL.self, forKey: .url) htmlString = try typeContainer.decodeIfPresent(String.self, forKey: .htmlString) @@ -51,6 +55,7 @@ import MVMCore public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(url, forKey: .url) diff --git a/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift b/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift index 85e4fffb..4fed14cb 100644 --- a/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift +++ b/MVMCoreUI/Atomic/Atoms/Views/WheelModel.swift @@ -19,6 +19,8 @@ public enum GraphStyle: String, Codable { public class WheelModel: MoleculeModelProtocol { public static var identifier: String = "wheel" + public var id: String = UUID().uuidString + public var style: GraphStyle = .unlimited { didSet { updateStyle() @@ -43,6 +45,7 @@ public class WheelModel: MoleculeModelProtocol { } private enum CodingKeys: String, CodingKey { + case id case style case size case diameter @@ -56,6 +59,8 @@ public class WheelModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let style = try typeContainer.decodeIfPresent(GraphStyle.self, forKey: .style) { self.style = style } @@ -84,6 +89,7 @@ public class WheelModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(style, forKey: .style) try container.encode(size, forKey: .size) diff --git a/MVMCoreUI/Atomic/Extensions/UIAccessibilityTraits+Codable.swift b/MVMCoreUI/Atomic/Extensions/UIAccessibilityTraits+Codable.swift new file mode 100644 index 00000000..4eb1ea2a --- /dev/null +++ b/MVMCoreUI/Atomic/Extensions/UIAccessibilityTraits+Codable.swift @@ -0,0 +1,125 @@ +// +// UIAccessibilityTraits+Codable.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 6/16/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation + +extension UIAccessibilityTraits: Codable { + private static func trait(from string: String) throws -> UIAccessibilityTraits { + switch string { + case "none": + return .none + case "button": + return .button + case "link": + return .link + case "image": + return .image + case "searchField": + return .searchField + case "keyboardKey": + return .keyboardKey + case "staticText": + return .staticText + case "header": + return .header + case "tabBar": + return .tabBar + case "summaryElement": + return .summaryElement + case "selected": + return .selected + case "notEnabled": + return .notEnabled + case "adjustable": + return .adjustable + case "allowsDirectInteraction": + return .allowsDirectInteraction + case "updatesFrequently": + return .updatesFrequently + case "causesPageTurn": + return .causesPageTurn + case "playsSound": + return .playsSound + case "startsMediaSession": + return .startsMediaSession + default: + throw ModelRegistry.Error.decoderOther(message: "Unsupported accessibility trait: \(string)") + } + } + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + // Iterate and decode each. + var accessibilityTrait: UIAccessibilityTraits = [] + while !container.isAtEnd { + let traitString = try container.decode(String.self) + let trait = try UIAccessibilityTraits.trait(from: traitString) + accessibilityTrait.insert(trait) + } + self = accessibilityTrait + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.unkeyedContainer() + if self.contains(.none) { + try container.encode("none") + } + if self.contains(.button) { + try container.encode("button") + } + if self.contains(.link) { + try container.encode("link") + } + if self.contains(.image) { + try container.encode("image") + } + if self.contains(.searchField) { + try container.encode("searchField") + } + if self.contains(.keyboardKey) { + try container.encode("keyboardKey") + } + if self.contains(.staticText) { + try container.encode("staticText") + } + if self.contains(.header) { + try container.encode("header") + } + if self.contains(.tabBar) { + try container.encode("tabBar") + } + if self.contains(.summaryElement) { + try container.encode("summaryElement") + } + if self.contains(.selected) { + try container.encode("selected") + } + if self.contains(.notEnabled) { + try container.encode("notEnabled") + } + if self.contains(.adjustable) { + try container.encode("adjustable") + } + if self.contains(.allowsDirectInteraction) { + try container.encode("allowsDirectInteraction") + } + if self.contains(.updatesFrequently) { + try container.encode("updatesFrequently") + } + if self.contains(.causesPageTurn) { + try container.encode("causesPageTurn") + } + if self.contains(.playsSound) { + try container.encode("playsSound") + } + if self.contains(.startsMediaSession) { + try container.encode("startsMediaSession") + } + } +} diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift index 25f59539..cdb61a4d 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1ButtonModel.swift @@ -7,7 +7,8 @@ // -public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { + //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -15,6 +16,10 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol { public var headlineBody: HeadlineBodyModel public var buttons: TwoButtonViewModel + public var children: [MoleculeModelProtocol] { + [headlineBody, buttons] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -25,6 +30,17 @@ public class HeadersH1ButtonModel: HeaderModel, MoleculeModelProtocol { super.init() } + //-------------------------------------------------- + // MARK: - Subclass + //-------------------------------------------------- + + public override func setDefaults() { + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } + super.setDefaults() + } + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift index d7603886..79705fd9 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1LandingPageHeaderModel.swift @@ -8,7 +8,7 @@ import Foundation -public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -20,6 +20,10 @@ public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol public var link: LinkModel public var buttons: TwoButtonViewModel + public var children: [MoleculeModelProtocol] { + [headline, headline2, subHeadline, body, link, buttons] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -32,6 +36,17 @@ public class HeadersH1LandingPageHeaderModel: HeaderModel, MoleculeModelProtocol self.buttons = buttons super.init() } + + //-------------------------------------------------- + // MARK: - Subclass + //-------------------------------------------------- + + public override func setDefaults() { + if headline.accessibilityTraits == nil { + headline.accessibilityTraits = .header + } + super.setDefaults() + } //-------------------------------------------------- // MARK: - Keys diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift index 864d88f1..9d246d98 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H1/HeadersH1NoButtonsBodyTextModel.swift @@ -8,7 +8,7 @@ -public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -16,6 +16,10 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol public static var identifier: String = "headerH1" public var headlineBody: HeadlineBodyModel + public var children: [MoleculeModelProtocol] { + [headlineBody] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -25,6 +29,17 @@ public class HeadersH1NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol super.init() } + //-------------------------------------------------- + // MARK: - Subclass + //-------------------------------------------------- + + public override func setDefaults() { + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } + super.setDefaults() + } + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift index d3ea7497..77bc25c1 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2ButtonsModel.swift @@ -9,7 +9,7 @@ import Foundation -public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +18,10 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol { public var headlineBody: HeadlineBodyModel public var buttons: TwoButtonViewModel + public var children: [MoleculeModelProtocol] { + [headlineBody, buttons] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -39,6 +43,9 @@ public class HeadersH2ButtonsModel: HeaderModel, MoleculeModelProtocol { if bottomPadding == nil { bottomPadding = Padding.Component.VerticalMarginSpacing } + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } super.setDefaults() } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift index 9d56118e..e0831a40 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2CaretLinkModel.swift @@ -7,7 +7,7 @@ // import Foundation -public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -15,6 +15,10 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol { public var headlineBody: HeadlineBodyModel public var caretLink: CaretLinkModel + public var children: [MoleculeModelProtocol] { + [headlineBody, caretLink] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -34,6 +38,9 @@ public class HeadersH2CaretLinkModel: HeaderModel, MoleculeModelProtocol { if bottomPadding == nil { bottomPadding = Padding.Component.VerticalMarginSpacing } + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } super.setDefaults() } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift index 66189f99..20d6afe2 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2LinkModel.swift @@ -8,7 +8,7 @@ import Foundation -public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -17,6 +17,10 @@ public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol { public var headlineBody: HeadlineBodyModel public var link: LinkModel + public var children: [MoleculeModelProtocol] { + [headlineBody, link] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -38,6 +42,9 @@ public class HeadersH2LinkModel: HeaderModel, MoleculeModelProtocol { if bottomPadding == nil { bottomPadding = Padding.Component.VerticalMarginSpacing } + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } super.setDefaults() } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift index 96d30730..86d38707 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2NoButtonsBodyTextModel.swift @@ -9,7 +9,7 @@ import Foundation -public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -17,6 +17,10 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol public static var identifier: String = "headerH2" public var headlineBody: HeadlineBodyModel + public var children: [MoleculeModelProtocol] { + [headlineBody] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -33,6 +37,9 @@ public class HeadersH2NoButtonsBodyTextModel: HeaderModel, MoleculeModelProtocol if bottomPadding == nil { bottomPadding = Padding.Component.VerticalMarginSpacing } + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } super.setDefaults() } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift index 5ca1eae1..ebca7e25 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2PricingTwoRowsModel.swift @@ -8,7 +8,7 @@ import Foundation -public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -21,6 +21,10 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol { public var body3: LabelModel public var subBody3: LabelModel? + public var children: [MoleculeModelProtocol] { + [headline, body, subBody, body2, subBody2, body3, subBody3].compactMap({$0}) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -45,6 +49,9 @@ public class HeadersH2PricingTwoRowsModel: HeaderModel, MoleculeModelProtocol { if bottomPadding == nil { bottomPadding = Padding.Component.VerticalMarginSpacing } + if headline.accessibilityTraits == nil { + headline.accessibilityTraits = .header + } super.setDefaults() subBody?.attributes = [LabelAttributeStrikeThroughModel(0, subBody?.text.count ?? 0)] subBody2?.attributes = [LabelAttributeStrikeThroughModel(0, subBody2?.text.count ?? 0)] diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift index ce27740b..e04ab07f 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/Headers/H2/HeadersH2TinyButtonModel.swift @@ -9,7 +9,7 @@ import Foundation -public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol { +public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +18,10 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol { public var headlineBody: HeadlineBodyModel public var button: ButtonModel + public var children: [MoleculeModelProtocol] { + [headlineBody, button] + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- @@ -39,6 +43,9 @@ public class HeadersH2TinyButtonModel: HeaderModel, MoleculeModelProtocol { if bottomPadding == nil { bottomPadding = Padding.Component.VerticalMarginSpacing } + if headlineBody.headline?.accessibilityTraits == nil { + headlineBody.headline?.accessibilityTraits = .header + } super.setDefaults() button.style = .secondary button.size = .small diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListNotificationAuthModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListNotificationAuthModel.swift new file mode 100644 index 00000000..4469d017 --- /dev/null +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/Miscellaneous/ListNotificationAuthModel.swift @@ -0,0 +1,69 @@ +// +// ListNotificationAuthModel.swift +// MVMCoreUI +// +// Created by Edayattu Salam, Nowfal on 13/04/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import Foundation + +public class ListNotificationAuthModel: ListOneColumnFullWidthTextAllTextAndLinksModel { + //-------------------------------------------------- + // MARK: - Properties + //-------------------------------------------------- + public class override var identifier: String { "listNotificationAuth" } + public var enableStatus: String + public var disableStatus: String + public var enableAction: ActionModelProtocol? + public var disableAction: ActionModelProtocol? + + //-------------------------------------------------- + // MARK: - Keys + //-------------------------------------------------- + private enum CodingKeys: String, CodingKey { + case enableStatus + case disableStatus + case enableAction + case disableAction + } + + //-------------------------------------------------- + // MARK: - Codec + //-------------------------------------------------- + required public init(from decoder: Decoder) throws { + let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + enableStatus = try typeContainer.decode(String.self, forKey: .enableStatus) + disableStatus = try typeContainer.decode(String.self, forKey: .disableStatus) + enableAction = try typeContainer.decodeModelIfPresent(codingKey: .enableAction) + disableAction = try typeContainer.decodeModelIfPresent(codingKey: .disableAction) + try super.init(from: decoder) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(enableStatus, forKey: .enableStatus) + try container.encode(disableStatus, forKey: .disableStatus) + try container.encodeModelIfPresent(enableAction, forKey: .enableAction) + try container.encodeModelIfPresent(disableAction, forKey: .disableAction) + } +} + +extension ListNotificationAuthModel: PageBehaviorProtocolRequirer { + public func getRequiredBehaviors() -> [PageBehaviorModelProtocol] { + [GetNotificationAuthStatusBehaviorModel()] + } +} + +extension ListNotificationAuthModel: GetNotificationAuthStatusBehaviorConsumerProtocol { + public func consume(notificationStatus: UNAuthorizationStatus) { + if notificationStatus == .authorized { + body?.text = enableStatus + action = enableAction + } else { + body?.text = disableStatus + action = disableAction + } + } +} diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextAllTextAndLinksModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextAllTextAndLinksModel.swift index 34e1d28b..1bdb2b41 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextAllTextAndLinksModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/List/OneColumn/ListOneColumnFullWidthTextAllTextAndLinksModel.swift @@ -13,8 +13,7 @@ public class ListOneColumnFullWidthTextAllTextAndLinksModel: ListItemModel, Mole //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- - - public static var identifier: String = "list1CTxt" + open class var identifier: String { "list1CTxt" } public var eyebrow: LabelModel? public var headline : LabelModel? public var subHeadline: LabelModel? diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockUpsPlanNamesModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockUpsPlanNamesModel.swift index 0d68da42..acbf276d 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockUpsPlanNamesModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockUpsPlanNamesModel.swift @@ -13,6 +13,8 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol { // MARK: - Properties //-------------------------------------------------- public static var identifier: String = "planNamesLockup" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var headline: LabelModel public var subHeadline: LabelModel @@ -31,6 +33,7 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol { // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case headline @@ -43,6 +46,7 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol { //-------------------------------------------------- required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) headline = try typeContainer.decode(LabelModel.self, forKey: .headline) subHeadline = try typeContainer.decode(LabelModel.self, forKey: .subHeadline) @@ -51,6 +55,7 @@ public class LockUpsPlanNamesModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(headline, forKey: .headline) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockupsPlanSMLXLModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockupsPlanSMLXLModel.swift index 433bc322..3e1c1bcc 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockupsPlanSMLXLModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/LockupsPlanSMLXLModel.swift @@ -14,6 +14,8 @@ public class LockupsPlanSMLXLModel: MoleculeModelProtocol { //-------------------------------------------------- public static var identifier: String = "planLockup" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var planLabel : LabelModel public var headline : LabelModel @@ -43,6 +45,7 @@ public class LockupsPlanSMLXLModel: MoleculeModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case planLabel @@ -57,6 +60,7 @@ public class LockupsPlanSMLXLModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) planLabel = try typeContainer.decode(LabelModel.self, forKey: .planLabel) headline = try typeContainer.decode(LabelModel.self, forKey: .headline) @@ -67,6 +71,7 @@ public class LockupsPlanSMLXLModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(planLabel, forKey: .planLabel) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift index 4548207d..89d32ca9 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/LockUps/TitleLockupModel.swift @@ -16,6 +16,7 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco public static var identifier: String = "titleLockup" public var moleculeName: String = TitleLockupModel.identifier + public var id: String = UUID().uuidString public var eyebrow: LabelModel? public var title: LabelModel @@ -130,6 +131,7 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case eyebrow @@ -145,6 +147,7 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString title = try typeContainer.decodeMolecule(codingKey: .title) eyebrow = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .eyebrow) subTitle = try typeContainer.decodeMoleculeIfPresent(codingKey: .subTitle) @@ -164,6 +167,7 @@ public class TitleLockupModel: MoleculeModelProtocol, ParentMoleculeModelProtoco public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(eyebrow, forKey: .eyebrow) try container.encodeModel(title, forKey: .title) diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsection.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsection.swift index 3f48c3d8..b73b7129 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsection.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsection.swift @@ -84,5 +84,6 @@ import Foundation } accessibilityLabel = message + accessibilityTraits.update(with: .header) } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift index 5040fbd4..8a039144 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnFullWidthTextDividerSubsectionModel.swift @@ -9,7 +9,7 @@ import Foundation -public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, MoleculeModelProtocol { +public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +18,10 @@ public class ListOneColumnFullWidthTextDividerSubsectionModel: ListItemModel, Mo public var headline: LabelModel public var body: LabelModel? + public var children: [MoleculeModelProtocol] { + [headline, body].compactMap({$0}) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShort.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShort.swift index 2f023b78..72dc78c0 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShort.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShort.swift @@ -84,5 +84,6 @@ import Foundation } accessibilityLabel = message + accessibilityTraits.update(with: .header) } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift index 66025760..6a40ebf7 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerShortModel.swift @@ -9,7 +9,7 @@ import Foundation -public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, MoleculeModelProtocol { +public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +18,10 @@ public class ListOneColumnTextWithWhitespaceDividerShortModel: ListItemModel, Mo public var headline: LabelModel public var body: LabelModel? + public var children: [MoleculeModelProtocol] { + [headline, body].compactMap({$0}) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTall.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTall.swift index 772ec5ac..169c33c9 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTall.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTall.swift @@ -84,5 +84,6 @@ import Foundation } accessibilityLabel = message + accessibilityTraits.update(with: .header) } } diff --git a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift index c713b0d2..09b462fe 100644 --- a/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift +++ b/MVMCoreUI/Atomic/Molecules/DesignedComponents/SectionDividers/OneColumn/ListOneColumnTextWithWhitespaceDividerTallModel.swift @@ -9,7 +9,7 @@ import Foundation -public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, MoleculeModelProtocol { +public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, MoleculeModelProtocol, ParentMoleculeModelProtocol { //-------------------------------------------------- // MARK: - Properties //-------------------------------------------------- @@ -18,6 +18,10 @@ public class ListOneColumnTextWithWhitespaceDividerTallModel: ListItemModel, Mol public var headline: LabelModel public var body: LabelModel? + public var children: [MoleculeModelProtocol] { + [headline, body].compactMap({$0}) + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartItemModel.swift b/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartItemModel.swift index ee4e664a..7ac68d0a 100644 --- a/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartItemModel.swift @@ -14,9 +14,11 @@ import Foundation // MARK: - Properties //-------------------------------------------------- - public var backgroundColor: Color? public static var identifier: String = "doughnutChartItem" public var moleculeName: String = DoughnutChartItemModel.identifier + @DecodableDefault.UUIDString public var id: String + + public var backgroundColor: Color? public var label: LabelModel @Percent public var percent: CGFloat public var color: Color diff --git a/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartModel.swift b/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartModel.swift index fbb034c6..3333ae77 100644 --- a/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Doughnut/DoughnutChartModel.swift @@ -14,9 +14,11 @@ import Foundation // MARK: - Properties //-------------------------------------------------- - public var backgroundColor: Color? public static var identifier: String = "doughnutChart" public var moleculeName: String = DoughnutChartModel.identifier + @DecodableDefault.UUIDString public var id: String + + public var backgroundColor: Color? public var title: LabelModel? public var subtitle: LabelModel? public var sections: [DoughnutChartItemModel] diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/ImageHeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/ImageHeadlineBodyModel.swift index 2c188d6b..84518c8c 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/ImageHeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/ImageHeadlineBodyModel.swift @@ -15,6 +15,8 @@ public class ImageHeadlineBodyModel: MoleculeModelProtocol { public static var identifier: String = "imageHeadlineBody" public var moleculeName: String = ImageHeadlineBodyModel.identifier + @DecodableDefault.UUIDString public var id: String + public var backgroundColor: Color? public var image: ImageViewModel public var headlineBody: HeadlineBodyModel diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift index 69bca3fc..571848dc 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/RadioButtonLabelModel.swift @@ -15,6 +15,8 @@ import MVMCore //-------------------------------------------------- public static var identifier: String = "radioButtonLabel" + @DecodableDefault.UUIDString public var id: String + public var backgroundColor: Color? public var moleculeName: String = RadioButtonLabelModel.identifier public var radioButton: RadioButtonModel diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift index 3b62210c..b984a9da 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabBarModel.swift @@ -11,6 +11,8 @@ import VDSColorTokens open class TabBarModel: MoleculeModelProtocol { public static var identifier: String = "tabBar" + public var id: String = UUID().uuidString + open var tabs: [TabBarItemModel] private var _backgroundColor: Color? @@ -58,6 +60,7 @@ open class TabBarModel: MoleculeModelProtocol { open var selectedTab: Int = 0 private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case tabs @@ -73,6 +76,7 @@ open class TabBarModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString tabs = try typeContainer.decode([TabBarItemModel].self, forKey: .tabs) if let color = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) { backgroundColor = color @@ -93,6 +97,7 @@ open class TabBarModel: MoleculeModelProtocol { open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(tabs, forKey: .tabs) try container.encodeIfPresent(_backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift index de9ef49f..e5682e71 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/Tabs.swift @@ -51,7 +51,7 @@ import VDSColorTokens public let selectionLineMovingTime: TimeInterval = 0.2 //------------------------------------------------- - // MARK:- Layout Views + // MARK: - Layout Views //------------------------------------------------- open override func reset() { @@ -122,8 +122,27 @@ import VDSColorTokens NSLayoutConstraint.constraintPinSubview(bottomLine, pinTop: false, pinBottom: true, pinLeft: true, pinRight: true) } + open override func layoutSubviews() { + super.layoutSubviews() + // Accounts for any collection size changes + DispatchQueue.main.async { + self.layoutCollection() + } + } + + /// Invalidates the layout and ensures we are paged to the correct cell. + open func layoutCollection() { + collectionView?.collectionViewLayout.invalidateLayout() + + // Go to current cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic. The dispatch is a sad way to ensure the collection view is ready to be scrolled. + DispatchQueue.main.async { + self.collectionView?.scrollToItem(at: IndexPath(row: self.selectedIndex, section: 0), at: .left, animated: false) + self.collectionView?.layoutIfNeeded() + } + } + //------------------------------------------------- - // MARK:- Control Methods + // MARK: - Control Methods //------------------------------------------------- public func selectIndex(_ index: Int, animated: Bool) { @@ -146,7 +165,7 @@ import VDSColorTokens } //------------------------------------------------- - // MARK:- Molecule Setup + // MARK: - Molecule Setup //------------------------------------------------- override open func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable : Any]?) { @@ -163,7 +182,7 @@ import VDSColorTokens } //------------------------------------------------- -// MARK:- Collection View Methods +// MARK: - Collection View Methods //------------------------------------------------- extension Tabs: UICollectionViewDataSource { @@ -235,7 +254,9 @@ extension Tabs: UICollectionViewDelegateFlowLayout { public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) { guard let tabCell = cell as? TabItemCell else { return } if indexPath.row == selectedIndex { - moveSelectionLine(toIndex: indexPath, animated: false, cell: tabCell) + DispatchQueue.main.async { + self.moveSelectionLine(toIndex: indexPath, animated: false, cell: tabCell) + } } } @@ -281,7 +302,7 @@ extension Tabs: UIScrollViewDelegate { //------------------------------------------------- -// MARK:- Bottom Line Methods +// MARK: - Bottom Line Methods //------------------------------------------------- extension Tabs { func moveSelectionLine(toIndex indexPath: IndexPath, animated: Bool, cell: TabItemCell) { diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift index 6e154895..3671e5e1 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TabsModel.swift @@ -11,6 +11,8 @@ import VDSColorTokens open class TabsModel: MoleculeModelProtocol { public static var identifier: String = "tabs" + public var id: String = UUID().uuidString + open var tabs: [TabItemModel] open var style: NavigationItemStyle? @@ -71,6 +73,7 @@ open class TabsModel: MoleculeModelProtocol { open var selectedIndex: Int = 0 private enum CodingKeys: String, CodingKey { + case id case moleculeName case tabs case backgroundColor @@ -87,6 +90,7 @@ open class TabsModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString tabs = try typeContainer.decode([TabItemModel].self, forKey: .tabs) backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) _selectedColor = try typeContainer.decodeIfPresent(Color.self, forKey: .selectedColor) @@ -100,6 +104,7 @@ open class TabsModel: MoleculeModelProtocol { open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encode(tabs, forKey: .tabs) try container.encodeIfPresent(_backgroundColor, forKey: .backgroundColor) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift index 718cfc30..b5d07c4a 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoButtonViewModel.swift @@ -15,6 +15,7 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { //-------------------------------------------------- public static var identifier: String = "twoButtonView" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var primaryButton: ButtonModel? public var secondaryButton: ButtonModel? @@ -28,6 +29,7 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case primaryButton @@ -49,6 +51,7 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) //set context value for 'primary' style to be set for the primaryButton in case the @@ -66,6 +69,7 @@ public class TwoButtonViewModel: ParentMoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(primaryButton, forKey: .primaryButton) diff --git a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoLinkViewModel.swift b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoLinkViewModel.swift index 57962c09..c9ae177b 100644 --- a/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoLinkViewModel.swift +++ b/MVMCoreUI/Atomic/Molecules/HorizontalCombinationViews/TwoLinkViewModel.swift @@ -10,11 +10,14 @@ import Foundation public class TwoLinkViewModel: MoleculeModelProtocol { public static var identifier: String = "twoLinkView" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var rightLink: LinkModel? public var leftLink: LinkModel? private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case rightLink @@ -28,6 +31,7 @@ public class TwoLinkViewModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) rightLink = try typeContainer.decodeIfPresent(LinkModel.self, forKey: .rightLink) leftLink = try typeContainer.decodeIfPresent(LinkModel.self, forKey: .leftLink) @@ -35,6 +39,7 @@ public class TwoLinkViewModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(rightLink, forKey: .rightLink) diff --git a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift index a37fe016..301a2274 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/CarouselItem.swift @@ -22,7 +22,8 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { open override func setupView() { super.setupView() - + clipsToBounds = true + // Covers the card when peaking. peakingCover.backgroundColor = .white peakingCover.alpha = 0 @@ -51,6 +52,12 @@ open class CarouselItem: MoleculeCollectionViewCell, CarouselItemProtocol { super.set(with: model, delegateObject, additionalData) guard let collectionModel = model as? CarouselItemModel else { return } + if let cornerRadius = (model as? ContainerModel)?.cornerRadius { + layer.cornerRadius = cornerRadius + } else { + layer.cornerRadius = 0 + } + // Handles peaking. allowsPeaking = collectionModel.peakingUI ?? false if let peakingArrowColor = collectionModel.peakingArrowColor { diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift index 5523d08d..2afda5a4 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionItemModel.swift @@ -16,6 +16,7 @@ open override class var identifier: String { "collectionItem" } public var action: ActionModelProtocol? + public var border = false //-------------------------------------------------- // MARK: - Keys @@ -23,6 +24,7 @@ private enum CodingKeys: String, CodingKey { case action + case border } //-------------------------------------------------- @@ -59,12 +61,16 @@ required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) action = try typeContainer.decodeModelIfPresent(codingKey: .action) + if let border = try typeContainer.decodeIfPresent(Bool.self, forKey: .border) { + self.border = border + } try super.init(from: decoder) } public override func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeModelIfPresent(action, forKey: .action) + try container.encodeIfPresent(border, forKey: .border) try super.encode(to: encoder) } } diff --git a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionViewCell.swift index 4ef4f84b..baad17f8 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/MoleculeCollectionViewCell.swift @@ -15,6 +15,13 @@ open class MoleculeCollectionViewCell: CollectionViewCell { super.set(with: model, delegateObject, additionalData) guard let collectionModel = model as? MoleculeCollectionItemModel else { return } + if collectionModel.border { + contentView.layer.borderColor = UIColor.black.cgColor + contentView.layer.borderWidth = 1 + } else { + contentView.layer.borderWidth = 0 + } + if molecule == nil { if let moleculeView = ModelRegistry.createMolecule(collectionModel.molecule, delegateObject: delegateObject, additionalData: additionalData) { addMolecule(moleculeView) diff --git a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift index 6a941881..e904707e 100644 --- a/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift +++ b/MVMCoreUI/Atomic/Molecules/Items/TabsTableViewCell.swift @@ -47,6 +47,7 @@ import UIKit public override func reset() { super.reset() tabs.reset() + tabs.paddingBeforeFirstTab = false } public override class func estimatedHeight(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?) -> CGFloat? { 46 } diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ActionDetailWithImageModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ActionDetailWithImageModel.swift index 7c8bd202..2e7b7500 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ActionDetailWithImageModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ActionDetailWithImageModel.swift @@ -11,6 +11,8 @@ import Foundation public class ActionDetailWithImageModel: MoleculeModelProtocol { public static var identifier: String = "actionDetailWithImage" public var moleculeName: String = ActionDetailWithImageModel.identifier + @DecodableDefault.UUIDString public var id: String + public var backgroundColor: Color? public var headlineBodyButton: HeadlineBodyButtonModel public var image: ImageViewModel diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift index aabd29e7..dc8f2396 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/CornerLabelsModel.swift @@ -11,6 +11,7 @@ import MVMCore public class CornerLabelsModel: ParentMoleculeModelProtocol { public static var identifier: String = "cornerLabels" + public var id: String = UUID().uuidString public var backgroundColor: Color? public var topLeftLabel: LabelModel? public var topRightLabel: LabelModel? @@ -26,6 +27,7 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol { } private enum CodingKeys: String, CodingKey { + case id case backgroundColor case topLeftLabel case topRightLabel @@ -37,6 +39,7 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) molecule = try typeContainer.decodeModelIfPresent(codingKey: .molecule) topLeftLabel = try typeContainer.decodeMoleculeIfPresent(codingKey: .topLeftLabel) @@ -47,6 +50,7 @@ public class CornerLabelsModel: ParentMoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeModelIfPresent(molecule, forKey: .molecule) try container.encodeModelIfPresent(topLeftLabel, forKey: .topLeftLabel) diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyLinkToggleModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyLinkToggleModel.swift index 19c1f4d6..84a03adf 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyLinkToggleModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyLinkToggleModel.swift @@ -10,6 +10,7 @@ import Foundation public class HeadlineBodyLinkToggleModel: MoleculeModelProtocol { public static var identifier: String = "headlineBodyLinkToggle" public var moleculeName: String = HeadlineBodyLinkToggleModel.identifier + @DecodableDefault.UUIDString public var id: String public var backgroundColor: Color? public var headlineBodyLink: HeadlineBodyLinkModel public var toggle: ToggleModel diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift index 800c976b..4c86ed76 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/HeadlineBodyToggleModel.swift @@ -12,6 +12,7 @@ import Foundation open class HeadlineBodyToggleModel: MoleculeModelProtocol { public static var identifier: String = "headlineBodyToggle" public var moleculeName: String = HeadlineBodyToggleModel.identifier + @DecodableDefault.UUIDString public var id: String open var backgroundColor: Color? open var headlineBody: HeadlineBodyModel open var toggle: ToggleModel diff --git a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/LabelToggleModel.swift b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/LabelToggleModel.swift index ee0c5f78..fe8cc16a 100644 --- a/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/LabelToggleModel.swift +++ b/MVMCoreUI/Atomic/Molecules/LeftRightViews/ToggleMolecules/LabelToggleModel.swift @@ -12,6 +12,8 @@ import MVMCore public class LabelToggleModel: MoleculeModelProtocol { public static var identifier: String = "labelToggle" public var moleculeName: String = LabelToggleModel.identifier + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var label: LabelModel public var toggle: ToggleModel @@ -22,6 +24,7 @@ public class LabelToggleModel: MoleculeModelProtocol { } private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case label @@ -30,6 +33,7 @@ public class LabelToggleModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey:.backgroundColor) label = try typeContainer.decode(LabelModel.self, forKey:.label) toggle = try typeContainer.decode(ToggleModel.self, forKey:.toggle) @@ -38,6 +42,7 @@ public class LabelToggleModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(label, forKey: .label) try container.encode(toggle, forKey: .toggle) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift index f0b0bbc0..ab1c993e 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationImageButtonModel.swift @@ -13,9 +13,11 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule // MARK: - Properties //-------------------------------------------------- + public static var identifier: String = "navigationImageButton" + public var id: String = UUID().uuidString + 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? @@ -35,6 +37,7 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case image case action case accessibilityIdentifier @@ -49,6 +52,7 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) image = try typeContainer.decode(String.self, forKey: .image) action = try typeContainer.decodeModel(codingKey: .action) @@ -60,6 +64,7 @@ public class NavigationImageButtonModel: NavigationButtonModelProtocol, Molecule open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(image, forKey: .image) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(moleculeName, forKey: .moleculeName) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift index d72c6ae0..33529e77 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/Buttons/NavigationLabelButtonModel.swift @@ -12,8 +12,10 @@ open class NavigationLabelButtonModel: NavigationButtonModelProtocol, MoleculeMo // MARK: - Properties //-------------------------------------------------- - open var backgroundColor: Color? open class var identifier: String { "navigationLabelButton" } + public var id: String = UUID().uuidString + + open var backgroundColor: Color? open var accessibilityIdentifier: String? open var title: String open var action: ActionModelProtocol @@ -32,6 +34,7 @@ open class NavigationLabelButtonModel: NavigationButtonModelProtocol, MoleculeMo //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case accessibilityIdentifier case title @@ -44,6 +47,7 @@ open class NavigationLabelButtonModel: NavigationButtonModelProtocol, MoleculeMo required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString accessibilityIdentifier = try typeContainer.decodeIfPresent(String.self, forKey: .accessibilityIdentifier) title = try typeContainer.decode(String.self, forKey: .title) action = try typeContainer.decodeModel(codingKey: .action) @@ -51,6 +55,7 @@ open class NavigationLabelButtonModel: NavigationButtonModelProtocol, MoleculeMo open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(accessibilityIdentifier, forKey: .accessibilityIdentifier) try container.encode(title, forKey: .title) diff --git a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift index 64b61480..d39cdeaf 100644 --- a/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift +++ b/MVMCoreUI/Atomic/Molecules/NavigationBar/NavigationItemModel.swift @@ -19,6 +19,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc //-------------------------------------------------- open class var identifier: String { "navigationBar" } + public var id: String = UUID().uuidString private let defaultHidesSystemBackButton = true @@ -74,6 +75,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case title case hidden @@ -96,6 +98,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString title = try typeContainer.decodeIfPresent(String.self, forKey: .title) if let hidden = try typeContainer.decodeIfPresent(Bool.self, forKey: .hidden) { self.hidden = hidden @@ -122,6 +125,7 @@ open class NavigationItemModel: NavigationItemModelProtocol, MoleculeModelProtoc open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(title, forKey: .title) try container.encode(hidden, forKey: .hidden) diff --git a/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMoleculeModel.swift index 931e7c04..b34922db 100644 --- a/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/OtherContainers/ModuleMoleculeModel.swift @@ -9,11 +9,15 @@ import Foundation open class ModuleMoleculeModel: MoleculeModelProtocol { - public var backgroundColor: Color? + public static var identifier: String = "moduleMolecule" + public var id: String = UUID().uuidString + + public var backgroundColor: Color? public var moduleName: String private enum CodingKeys: String, CodingKey { + case id case moduleName } @@ -23,11 +27,13 @@ open class ModuleMoleculeModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString moduleName = try typeContainer.decode(String.self, forKey:.moduleName) } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moduleName, forKey: .moduleName) } } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift index 8c9416c0..4c677029 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift @@ -7,6 +7,9 @@ // import Foundation +import Combine +import Dispatch +import MVMCore @objcMembers open class CollapsableNotification: View { //-------------------------------------------------- @@ -14,8 +17,11 @@ import Foundation //-------------------------------------------------- public let topView = CollapsableNotificationTopView() - public let bottomView = NotificationView() + public let bottomView = NotificationMoleculeView() public var verticalStack: UIStackView! + + public var cancellables = Set() + private var timerSource: DispatchSourceTimer? //-------------------------------------------------- // MARK: - Life Cycle @@ -33,6 +39,7 @@ import Foundation NSLayoutConstraint.constraintPinSubview(verticalStack, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0) reset() + subscribeForNotifications() } open override func updateView(_ size: CGFloat) { @@ -46,6 +53,42 @@ import Foundation backgroundColor = .mvmGreen() } + open func subscribeForNotifications() { + // Resets state when about to show. + NotificationHandler.shared()?.onNotificationWillShow.receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] (view, _) in + guard let self = self, + self == view else { return } + self.initialState() + }).store(in: &cancellables) + // Begins the collapse timer when shown. + NotificationHandler.shared()?.onNotificationShown.receive(on: DispatchQueue.main) + .sink(receiveValue: { [weak self] (view, _) in + guard let self = self, + self == view, + let model = self.model as? CollapsableNotificationModel else { return } + if !model.initiallyCollapsed { + self.autoCollapse() + } + }).store(in: &cancellables) + // Cancels any collapse timer when dismissing + NotificationHandler.shared()?.onNotificationWillDismiss.sink(receiveValue: { [weak self] (view, _) in + guard let self = self, + self == view else { return } + timerSource?.cancel() + timerSource = nil + }).store(in: &cancellables) + } + + /// Set initial collapse/expand state. + open func initialState() { + guard let model = model as? CollapsableNotificationModel else { return } + topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed + topView.button.isUserInteractionEnabled = model.initiallyCollapsed + bottomView.isHidden = model.initiallyCollapsed + verticalStack.layoutIfNeeded() + } + //-------------------------------------------------- // MARK: - Molecule //-------------------------------------------------- @@ -66,53 +109,52 @@ import Foundation self?.expand(topViewShowing: model.alwaysShowTopLabel) } } - - // Set initial collapse/expand state. - topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed - topView.button.isUserInteractionEnabled = model.initiallyCollapsed - bottomView.isHidden = model.initiallyCollapsed - verticalStack.layoutIfNeeded() - - if !model.initiallyCollapsed { - autoCollapse() - } + initialState() } open func performBlockOperation(with block: @escaping (MVMCoreBlockOperation) -> Void) { let operation = MVMCoreBlockOperation(block: block)! - MVMCoreNavigationHandler.shared()?.addNavigationOperation(operation) + NavigationHandler.shared().navigationQueue.addOperation(operation) } /// Collapses after a delay open func autoCollapse() { let delay: DispatchTimeInterval = DispatchTimeInterval.seconds((model as? CollapsableNotificationModel)?.collapseTime ?? 5) - DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in + timerSource = DispatchSource.makeTimerSource() + timerSource?.setEventHandler(handler: { [weak self] in // If accessibility focused, delay collapse. guard let self = self else { return } - if MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { - NotificationCenter.default.addObserver(self, selector: #selector(self.accessibilityFocusChanged(notification:)), name: UIAccessibility.elementFocusedNotification, object: nil) - } else { - self.collapse() - } - } + self.timerSource = nil + MVMCoreDispatchUtility.performBlock(onMainThread: { + if MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { + NotificationCenter.default.addObserver(self, selector: #selector(self.accessibilityFocusChanged(notification:)), name: UIAccessibility.elementFocusedNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(self.accessibilityFocusChanged(notification:)), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil) + + } else { + self.collapse() + } + }) + }) + timerSource?.schedule(deadline: .now() + delay) + timerSource?.activate() } /// Collapses to show just the top view. open func collapse(animated: Bool = true) { guard !bottomView.isHidden else { return } performBlockOperation { [weak self] (operation) in - let strongSelf = self + guard let strongSelf = self else { return } MVMCoreDispatchUtility.performBlock(onMainThread: { - MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() + strongSelf.superview?.superview?.layoutIfNeeded() let animation = { - strongSelf?.topView.isHidden = false - strongSelf?.bottomView.isHidden = true - strongSelf?.verticalStack.layoutIfNeeded() + strongSelf.topView.isHidden = false + strongSelf.bottomView.isHidden = true + strongSelf.verticalStack.layoutIfNeeded() } let completion: (Bool) -> Void = { (finished) in - strongSelf?.topView.button.isUserInteractionEnabled = true - MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() - UIAccessibility.post(notification: .layoutChanged, argument: strongSelf?.getAccessibilityLayoutChangedArgument()) + strongSelf.topView.button.isUserInteractionEnabled = true + strongSelf.superview?.superview?.layoutIfNeeded() + UIAccessibility.post(notification: .layoutChanged, argument: strongSelf.getAccessibilityLayoutChangedArgument()) operation.markAsFinished() } @@ -130,19 +172,19 @@ import Foundation open func expand(topViewShowing: Bool = false, animated: Bool = true) { guard bottomView.isHidden else { return } performBlockOperation { [weak self] (operation) in - let strongSelf = self + guard let strongSelf = self else { return } MVMCoreDispatchUtility.performBlock(onMainThread: { - MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() - strongSelf?.topView.button.isUserInteractionEnabled = false + strongSelf.superview?.superview?.layoutIfNeeded() + strongSelf.topView.button.isUserInteractionEnabled = false let animation = { - strongSelf?.topView.isHidden = !topViewShowing - strongSelf?.bottomView.isHidden = false - strongSelf?.verticalStack.layoutIfNeeded() + strongSelf.topView.isHidden = !topViewShowing + strongSelf.bottomView.isHidden = false + strongSelf.verticalStack.layoutIfNeeded() } let completion: (Bool) -> Void = { (finished) in - MVMCoreUITopAlertView.sharedGlobal()?.superview?.layoutIfNeeded() - UIAccessibility.post(notification: .layoutChanged, argument: strongSelf?.getAccessibilityLayoutChangedArgument()) - strongSelf?.autoCollapse() + strongSelf.superview?.superview?.layoutIfNeeded() + UIAccessibility.post(notification: .layoutChanged, argument: strongSelf.getAccessibilityLayoutChangedArgument()) + strongSelf.autoCollapse() operation.markAsFinished() } @@ -161,16 +203,17 @@ import Foundation } /// Collapse if focus is no longer on this top alert. - @objc func accessibilityFocusChanged(notification: Notification) { - if (notification.userInfo?[UIAccessibility.focusedElementUserInfoKey] != nil) && !MVMCoreUIUtility.viewContainsAccessiblityFocus(self) { + @objc private func accessibilityFocusChanged(notification: Notification) { + if !UIAccessibility.isVoiceOverRunning || ((notification.userInfo?[UIAccessibility.focusedElementUserInfoKey] != nil) && !MVMCoreUIUtility.viewContainsAccessiblityFocus(self)) { NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil) collapse() } } } extension CollapsableNotification: StatusBarUI { - func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) { + public func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) { let color = backgroundColor ?? UIColor.mvmGreen var greyScale: CGFloat = 0 topView.label.textColor.getWhite(&greyScale, alpha: nil) @@ -187,3 +230,10 @@ extension CollapsableNotification: AccessibilityProtocol { } } } + +extension CollapsableNotification: CollapsableNotificationProtocol { + @MainActor + public func collapse() { + collapse(animated: true) + } +} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift index 511f41a5..3d366347 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift @@ -7,8 +7,9 @@ // import Foundation +import MVMCore -open class CollapsableNotificationModel: NotificationModel { +open class CollapsableNotificationModel: NotificationMoleculeModel { public class override var identifier: String { return "collapsableNotification" } @@ -17,11 +18,14 @@ open class CollapsableNotificationModel: NotificationModel { public var alwaysShowTopLabel = false public var collapseTime: Int = 5 public var initiallyCollapsed = false - public var pages: [String]? - public init(with topLabel: LabelModel, headline: LabelModel) { + public init(with topLabel: LabelModel, headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, topAction: ActionModelProtocol? = nil, collapseTime: Int? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil) { self.topLabel = topLabel - super.init(with: headline) + self.topAction = topAction + if let collapseTime = collapseTime { + self.collapseTime = collapseTime + } + super.init(with: headline, style: style, backgroundColor: backgroundColor, body: body, button: button, closeButton: closeButton) } open override func setDefaults() { @@ -30,7 +34,12 @@ open class CollapsableNotificationModel: NotificationModel { topLabel.numberOfLines = 1 } if topLabel.textColor == nil { - topLabel.textColor = Color(uiColor: .white) + switch style { + case .error, .warning: + topLabel.textColor = Color(uiColor: .mvmBlack) + default: + topLabel.textColor = Color(uiColor: .mvmWhite) + } } if topLabel.textAlignment == nil { topLabel.textAlignment = .center @@ -44,7 +53,6 @@ open class CollapsableNotificationModel: NotificationModel { case alwaysShowTopLabel case collapseTime case initiallyCollapsed - case pages } required public init(from decoder: Decoder) throws { @@ -60,7 +68,6 @@ open class CollapsableNotificationModel: NotificationModel { if let initiallyCollapsed = try typeContainer.decodeIfPresent(Bool.self, forKey: .initiallyCollapsed) { self.initiallyCollapsed = initiallyCollapsed } - pages = try typeContainer.decodeIfPresent([String].self, forKey: .pages) try super.init(from: decoder) } @@ -73,6 +80,5 @@ open class CollapsableNotificationModel: NotificationModel { try container.encode(alwaysShowTopLabel, forKey: .alwaysShowTopLabel) try container.encode(collapseTime, forKey: .collapseTime) try container.encode(initiallyCollapsed, forKey: .initiallyCollapsed) - try container.encodeIfPresent(pages, forKey: .pages) } } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift index b3223fc7..01c0b3d5 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift @@ -60,7 +60,7 @@ import Foundation isAccessibilityElement = true accessibilityLabel = label.text accessibilityTraits = (button.isUserInteractionEnabled && button.actionModel != nil) ? .button : .none - MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: self) + NotificationMoleculeView.amendAccesibilityLabel(for: self) } @objc func pressed(_ sender: Notification) { diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift deleted file mode 100644 index e1af54e3..00000000 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertExpandableView+Extension.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// MVMCoreUITopAlertExpandableView+Extension.swift -// MVMCoreUI -// -// Created by Scott Pfeil on 9/15/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// - - -extension MVMCoreUITopAlertExpandableView: MoleculeViewProtocol { - - 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 - collapseTime = model.collapseTime - onlyShowTopMessageWhenCollapsed = !model.alwaysShowTopLabel - - var actionMap = model.button?.action.toJSON() - if let title = model.button?.title { - actionMap?.updateValue(title, forKey: KeyTitle) - } - buttonView?.setupCloseButton(model.closeButton != nil, animationDelegate: MVMCoreUITopAlertView.sharedGlobal()?.animationDelegate) - setTopMessage(model.topLabel.text, message: model.headline.text, subMessage: model.body?.text, contentColor: model.headline.textColor?.uiColor ?? .white, actionMap: actionMap, additionalData: nil) - expand(false) - - if let button = shortView?.button, - let topActionMap = model.topAction?.toJSON() { - MVMCoreUITopAlertBaseView.addAction(to: button, actionMap: topActionMap, additionalData: nil) - shortView?.label?.accessibilityTraits = .button - } - - if let accessibilityIdentifier = model.accessibilityIdentifier { - self.accessibilityIdentifier = accessibilityIdentifier - } - } -} - -extension MVMCoreUITopAlertExpandableView: StatusBarUI { - func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) { - if shortView?.label?.text?.count ?? 0 > 0 { - let color = backgroundColor ?? UIColor.mvmGreen - var greyScale: CGFloat = 0 - if shortView?.label?.textColor.getWhite(&greyScale, alpha: nil) ?? false { - return (color, greyScale > 0.5 ? .lightContent : .default) - } else { - return (color, .default) - } - } else { - return (.white, .default) - } - } -} diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift deleted file mode 100644 index c6109372..00000000 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/MVMCoreUITopAlertMainView+Extension.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// MVMCoreUITopAlertMainView+Extension.swift -// MVMCoreUI -// -// Created by Scott Pfeil on 9/15/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// - - -extension MVMCoreUITopAlertMainView: MoleculeViewProtocol { - - 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/NotificationMoleculeModel.swift similarity index 89% rename from MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift rename to MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift index fe40a022..d26bb06c 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift @@ -1,5 +1,5 @@ // -// NotificationModel.swift +// NotificationMoleculeModel.swift // MVMCoreUI // // Created by Scott Pfeil on 9/15/20. @@ -7,7 +7,7 @@ // -open class NotificationModel: ContainerModel, MoleculeModelProtocol { +open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol { /** The style of the notification: @@ -34,14 +34,19 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { public var body: LabelModel? public var button: ButtonModel? public var closeButton: NotificationXButtonModel? - public var style: NotificationModel.Style = .success + public var style: NotificationMoleculeModel.Style = .success //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- - public init(with headline: LabelModel) { + public init(with headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil) { self.headline = headline + self.style = style + self.backgroundColor = backgroundColor + self.body = body + self.button = button + self.closeButton = closeButton super.init() } @@ -132,7 +137,7 @@ open class NotificationModel: ContainerModel, MoleculeModelProtocol { body = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .body) button = try typeContainer.decodeIfPresent(ButtonModel.self, forKey: .button) closeButton = try typeContainer.decodeIfPresent(NotificationXButtonModel.self, forKey: .closeButton) - if let style = try typeContainer.decodeIfPresent(NotificationModel.Style.self, forKey: .style) { + if let style = try typeContainer.decodeIfPresent(NotificationMoleculeModel.Style.self, forKey: .style) { self.style = style } super.init() diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift similarity index 77% rename from MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift rename to MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift index c17f43e0..c731f6d6 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift @@ -1,5 +1,5 @@ // -// Notification.swift +// NotificationMoleculeView.swift // MVMCoreUI // // Created by Scott Pfeil on 9/16/20. @@ -8,7 +8,7 @@ import Foundation -@objcMembers open class NotificationView: Container { +@objcMembers open class NotificationMoleculeView: Container { //-------------------------------------------------- // MARK: - Outlets //-------------------------------------------------- @@ -63,7 +63,7 @@ import Foundation open override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) { super.set(with: model, delegateObject, additionalData) - guard let model = model as? NotificationModel else { return } + guard let model = model as? NotificationMoleculeModel else { return } labelStack.updateContainedMolecules(with: [model.headline, model.body], delegateObject, nil) horizontalStack.updateContainedMolecules(with: [labelStack.stackModel, model.button, model.closeButton], delegateObject, nil) updateAccessibility() @@ -74,13 +74,22 @@ import Foundation } open func updateAccessibility() { - MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: headline) - MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: body) - MVMCoreUITopAlertBaseView.amendAccesibilityLabel(for: button) + NotificationMoleculeView.amendAccesibilityLabel(for: headline) + NotificationMoleculeView.amendAccesibilityLabel(for: body) + NotificationMoleculeView.amendAccesibilityLabel(for: button) + NotificationMoleculeView.amendAccesibilityLabel(for: closeButton) + } + + /// Formats the accessibilityLabel so voice over users know it's in the notification. + static public func amendAccesibilityLabel(for view: UIView) { + guard let amendment = MVMCoreUIUtility.hardcodedString(withKey: "top_alert_notification"), + let accessibilityLabel = view.accessibilityLabel, + !accessibilityLabel.hasPrefix(amendment) else { return } + view.accessibilityLabel = "\(amendment) - \(accessibilityLabel)" } } -extension NotificationView: AccessibilityProtocol { +extension NotificationMoleculeView: AccessibilityProtocol { public func getAccessibilityLayoutChangedArgument() -> Any? { return headline } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift index 59da1868..ac1c8905 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationXButtonModel.swift @@ -7,23 +7,32 @@ // import Foundation +import MVMCore public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol { + public static var identifier: String = "notificationXButton" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var color: Color? public var action: ActionModelProtocol = ActionNoopModel() private enum CodingKeys: String, CodingKey { + case id case moleculeName case color case action } - public init() {} + public init(color: Color? = nil, action: ActionModelProtocol = ActionNoopModel()) { + self.color = color + self.action = action + } public required init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString color = try typeContainer.decodeIfPresent(Color.self, forKey: .color) if let action: ActionModelProtocol = try typeContainer.decodeModelIfPresent(codingKey: .action) { self.action = action @@ -32,6 +41,7 @@ public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtoco public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(color, forKey: .color) try container.encodeModel(action, forKey: .action) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift index 83f5b4bd..0d46916e 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/EyebrowHeadlineBodyLinkModel.swift @@ -13,6 +13,7 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule //-------------------------------------------------- public static var identifier: String = "eyebrowHeadlineBodyLink" + public var id: String = UUID().uuidString public var moleculeName: String = EyebrowHeadlineBodyLinkModel.identifier public var backgroundColor: Color? public var eyebrow: LabelModel? @@ -56,6 +57,7 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case eyebrow @@ -70,6 +72,7 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) eyebrow = try typeContainer.decodeIfPresent(LabelModel.self, forKey: .eyebrow) headline = try typeContainer.decodeMoleculeIfPresent(codingKey: .headline) @@ -84,6 +87,7 @@ public class EyebrowHeadlineBodyLinkModel: MoleculeModelProtocol, ParentMolecule public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeIfPresent(eyebrow, forKey: .eyebrow) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyButtonModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyButtonModel.swift index 70a08d3b..1b713720 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyButtonModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyButtonModel.swift @@ -14,6 +14,8 @@ public class HeadlineBodyButtonModel: MoleculeModelProtocol { public static var identifier: String = "headlineBodyButton" public var moleculeName: String = HeadlineBodyButtonModel.identifier + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var headlineBody: HeadlineBodyModel @@ -45,6 +47,7 @@ public class HeadlineBodyButtonModel: MoleculeModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case headlineBody @@ -58,6 +61,7 @@ public class HeadlineBodyButtonModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) headlineBody = try typeContainer.decode(HeadlineBodyModel.self, forKey: .headlineBody) button = try typeContainer.decode(ButtonModel.self, forKey: .button) @@ -67,6 +71,7 @@ public class HeadlineBodyButtonModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(headlineBody, forKey: .headlineBody) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyLinkModel.swift index 9f16e3ce..6c2113bf 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyLinkModel.swift @@ -16,6 +16,8 @@ public class HeadlineBodyLinkModel: MoleculeModelProtocol { public static var identifier: String = "headlineBodyLink" public var moleculeName: String = HeadlineBodyLinkModel.identifier + @DecodableDefault.UUIDString public var id: String + public var headlineBody: HeadlineBodyModel public var link: LinkModel public var backgroundColor: Color? diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift index 20df223a..a3eaf918 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/HeadlineBodyModel.swift @@ -14,6 +14,7 @@ public static var identifier: String = "headlineBody" public var moleculeName: String = HeadlineBodyModel.identifier + public var id: String = UUID().uuidString public var headline: LabelModel? public var body: LabelModel? public var style: Style? @@ -23,6 +24,15 @@ [headline, body].compactMap { $0 } } + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) -> Bool { + return [ + \HeadlineBodyModel.headline, + \HeadlineBodyModel.body, + ].contains { + replaceChildMolecule(on: self, keyPath: $0, replacementMolecule: replacementMolecule) + } + } + //-------------------------------------------------- // MARK: - Enum //-------------------------------------------------- @@ -57,6 +67,7 @@ //----------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case headline case body @@ -66,6 +77,7 @@ required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString headline = try typeContainer.decodeMoleculeIfPresent(codingKey: .headline) body = try typeContainer.decodeMoleculeIfPresent(codingKey: .body) style = try typeContainer.decodeIfPresent(Style.self, forKey: .style) @@ -74,6 +86,7 @@ public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(headline, forKey: .headline) try container.encodeIfPresent(body, forKey: .body) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeModel.swift index b1b12a54..8ffc24e4 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/Lists/StringAndMoleculeStack/StringAndMoleculeModel.swift @@ -13,6 +13,8 @@ public class StringAndMoleculeModel: MoleculeModelProtocol { //-------------------------------------------------- public static var identifier: String = "stringAndMoleculeModel" + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var string: String public var molecule: MoleculeModelProtocol @@ -33,6 +35,7 @@ public class StringAndMoleculeModel: MoleculeModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case string @@ -46,6 +49,7 @@ public class StringAndMoleculeModel: MoleculeModelProtocol { public required init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) string = try typeContainer.decode(String.self, forKey: .string) molecule = try typeContainer.decodeModel(codingKey: .molecule) @@ -54,6 +58,7 @@ public class StringAndMoleculeModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(string, forKey: .string) try container.encodeModel(molecule, forKey: .molecule) diff --git a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/ThreeHeadlineBodyLinkModel.swift b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/ThreeHeadlineBodyLinkModel.swift index efdc058c..8f413429 100644 --- a/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/ThreeHeadlineBodyLinkModel.swift +++ b/MVMCoreUI/Atomic/Molecules/VerticalCombinationViews/ThreeHeadlineBodyLinkModel.swift @@ -16,6 +16,8 @@ public class ThreeHeadlineBodyLinkModel: MoleculeModelProtocol { public static var identifier: String = "threeHeadlineBodyLink" public var moleculeName: String = ThreeHeadlineBodyLinkModel.identifier + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var headline1: LabelModel @@ -41,6 +43,7 @@ public class ThreeHeadlineBodyLinkModel: MoleculeModelProtocol { //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case headline1 @@ -56,6 +59,7 @@ public class ThreeHeadlineBodyLinkModel: MoleculeModelProtocol { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString backgroundColor = try typeContainer.decodeIfPresent(Color.self, forKey: .backgroundColor) headline1 = try typeContainer.decode(LabelModel.self, forKey: .headline1) headline2 = try typeContainer.decode(LabelModel.self, forKey: .headline2) @@ -67,6 +71,7 @@ public class ThreeHeadlineBodyLinkModel: MoleculeModelProtocol { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(moleculeName, forKey: .moleculeName) + try container.encode(id, forKey: .id) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encode(headline1, forKey: .headline1) try container.encode(headline2, forKey: .headline2) diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift index fcaa1010..ea7d89b8 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/Carousel.swift @@ -104,6 +104,7 @@ open class Carousel: View { super.setupView() collectionView.dataSource = self collectionView.delegate = self + collectionView.bounces = false addSubview(collectionView) bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300) @@ -129,7 +130,7 @@ open class Carousel: View { inset.left = carouselModel?.leftPadding ?? Padding.Component.horizontalPaddingForSize(size) inset.right = carouselModel?.rightPadding ?? Padding.Component.horizontalPaddingForSize(size) } - (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset = inset + collectionView.contentInset = inset // Update cells and re-layout. for cell in collectionView.visibleCells { diff --git a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift index bc3877e5..5bfd8d0f 100644 --- a/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift +++ b/MVMCoreUI/Atomic/Organisms/Carousel/CarouselModel.swift @@ -19,6 +19,8 @@ import UIKit return "carousel" } + public var id: String = UUID().uuidString + public var backgroundColor: Color? public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol] public var index: Int = 0 @@ -78,6 +80,7 @@ import UIKit //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case moleculeName case backgroundColor case molecules @@ -108,6 +111,7 @@ import UIKit required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString molecules = try typeContainer.decodeModels(codingKey: .molecules) index = try typeContainer.decodeIfPresent(Int.self, forKey: .index) ?? 0 selectable = try typeContainer.decodeIfPresent(Bool.self, forKey: .selectable) ?? false @@ -144,6 +148,7 @@ import UIKit public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encode(moleculeName, forKey: .moleculeName) try container.encodeIfPresent(backgroundColor, forKey: .backgroundColor) try container.encodeModels(molecules, forKey: .molecules) diff --git a/MVMCoreUI/Atomic/Organisms/StackModel.swift b/MVMCoreUI/Atomic/Organisms/StackModel.swift index 6310fcea..da6c067e 100644 --- a/MVMCoreUI/Atomic/Organisms/StackModel.swift +++ b/MVMCoreUI/Atomic/Organisms/StackModel.swift @@ -25,6 +25,15 @@ return molecules } + public func replaceChildMolecule(with replacementMolecule: MoleculeModelProtocol) -> Bool { + guard let replacementMolecule = replacementMolecule as? StackItemModelProtocol & MoleculeModelProtocol else { return false } + guard let matchingIndex = molecules.firstIndex(where: { molecule in + molecule.id == replacementMolecule.id + }) else { return false } + molecules[matchingIndex] = replacementMolecule + return true + } + //-------------------------------------------------- // MARK: - Initializer //-------------------------------------------------- diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift index 6845d44b..31adc6ef 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/MoleculeModelProtocol.swift @@ -5,10 +5,10 @@ public enum MolecularError: Swift.Error { case countImbalance(String) } - public protocol MoleculeModelProtocol: ModelProtocol, AccessibilityModelProtocol, MoleculeTreeTraversalProtocol, MoleculeMaskingProtocol { var moleculeName: String { get } var backgroundColor: Color? { get set } + var id: String { get } } public extension MoleculeModelProtocol { @@ -20,7 +20,6 @@ public extension MoleculeModelProtocol { static var categoryCodingKey: String { "moleculeName" } } - // Helpers made due to swift not able to reconcile which category. extension KeyedDecodingContainer where Key: CodingKey { /// Decodes to a registered molecule based on the identifier @@ -53,8 +52,9 @@ public extension MoleculeModelProtocol { } // Base case. No additional children to traverse. - func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol)->Void) { - onVisit(depth, self) + func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool)->Void) { + var stop = false + onVisit(depth, self, &stop) } } @@ -67,7 +67,7 @@ public extension Array where Element == MoleculeModelProtocol { } } - func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol)->Void) { + func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool) -> Void) { forEach { (molecule) in molecule.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit) } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift index 2ec05db4..78377a25 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/ParentMoleculeModelProtocol.swift @@ -8,10 +8,11 @@ import Foundation -public protocol ParentMoleculeModelProtocol: MoleculeModelProtocol { +public protocol ParentMoleculeModelProtocol: MoleculeModelProtocol, AnyObject { var children: [MoleculeModelProtocol] { get } + func replaceChildMolecule(with molecule: MoleculeModelProtocol) -> Bool } public extension ParentMoleculeModelProtocol { @@ -35,21 +36,44 @@ public extension ParentMoleculeModelProtocol { return result } - func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol)->Void) { + func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool)->Void) { + var stop = false if (options == .parentFirst) { - onVisit(depth, self) + onVisit(depth, self, &stop) + guard !stop else { return } } - children.forEach { (molecule) in - if let additionalParent = molecule as? ParentMoleculeModelProtocol { + for child in children { + if let additionalParent = child as? ParentMoleculeModelProtocol { // Safety net to make sure the ParentMoleculeModelProtocol's method extension is called over the base MoleculeModelProtocol. additionalParent.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit) } else { - molecule.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit) + child.depthFirstTraverse(options: options, depth: depth + 1, onVisit: onVisit) } + guard !stop else { return } } if (options == .childFirst) { - onVisit(depth, self) + onVisit(depth, self, &stop) } // if options == .leafOnly don't call on self. } + + /// Top level test to replace child molecules. Each parent molecule should attempt to replace. + func replaceChildMolecule(with molecule: MoleculeModelProtocol) -> Bool { return false } + + /// Helper function for replacing molecules on a path. + func replaceChildMolecule(on target: P, keyPath: ReferenceWritableKeyPath, replacementMolecule: MoleculeModelProtocol) -> Bool { + if let currentMolecule = target[keyPath: keyPath], currentMolecule.id == replacementMolecule.id, let newHeadline = replacementMolecule as? T { + target[keyPath: keyPath] = newHeadline + return true + } + return false + } + + func replaceChildMolecule(on target: P, keyPath: ReferenceWritableKeyPath, replacementMolecule: MoleculeModelProtocol) -> Bool { + if target[keyPath: keyPath].id == replacementMolecule.id, let newHeadline = replacementMolecule as? T { + target[keyPath: keyPath] = newHeadline + return true + } + return false + } } diff --git a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift index 531a48d1..2cd233b0 100644 --- a/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/ModelProtocols/TemplateModelProtocol.swift @@ -30,7 +30,7 @@ public extension TemplateModelProtocol { return rootMolecules.reduceDepthFirstTraverse(options: options, depth: depth, initialResult: initialResult, nextPartialResult: nextPartialResult) } - func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol) -> Void) { + func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool) -> Void) { return rootMolecules.depthFirstTraverse(options: options, depth: depth, onVisit: onVisit) } } diff --git a/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift b/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift index d10a2d19..a2da7cf4 100644 --- a/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift +++ b/MVMCoreUI/Atomic/Protocols/MoleculeTreeTraversalProtocol.swift @@ -19,7 +19,7 @@ public protocol MoleculeTreeTraversalProtocol { func reduceDepthFirstTraverse(options: TreeTraversalOptions, depth: Int, initialResult: Result, nextPartialResult: (Result, MoleculeModelProtocol, Int)->Result) -> Result - func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol)->Void) + func depthFirstTraverse(options: TreeTraversalOptions, depth: Int, onVisit: (Int, MoleculeModelProtocol, inout Bool)->Void) //func breadthFirstTraverse() } @@ -37,7 +37,7 @@ public extension MoleculeTreeTraversalProtocol { } func printMolecules(options: TreeTraversalOptions = .parentFirst) { - depthFirstTraverse(options: options, depth: 1) { (depth, molecule) in + depthFirstTraverse(options: options, depth: 1) { depth, molecule, stop in print("\(String(repeating: ">>", count: depth)) \"\(molecule.moleculeName)\" [\(molecule)]") } } @@ -50,4 +50,11 @@ public extension MoleculeTreeTraversalProtocol { return accumulator } } + + func replaceMolecule(with replacementMolecule: MoleculeModelProtocol) { + depthFirstTraverse(options: .parentFirst, depth: 0) { depth, molecule, stop in + guard let parentMolecule = molecule as? ParentMoleculeModelProtocol else { return } + stop = parentMolecule.replaceChildMolecule(with: replacementMolecule) + } + } } diff --git a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift index 6188aeae..b84792a6 100644 --- a/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/CollectionTemplate.swift @@ -100,7 +100,8 @@ let columns = templateModel?.columns, columns > 0, let cell = cell as? CollectionTemplateItemProtocol { let width = (size - collectionView.adjustedContentInset.left - collectionView.adjustedContentInset.right) / CGFloat(columns) - cell.set(width: width) + // Makes the width slightly less to avoid rounding errors and the column being a little too big. + cell.set(width: width - 0.1) } } diff --git a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift index cace752a..950b6785 100644 --- a/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift +++ b/MVMCoreUI/Atomic/Templates/MoleculeListTemplate.swift @@ -72,7 +72,11 @@ open class MoleculeListTemplate: ThreeLayerTableViewController, TemplateProtocol override open func viewForBottom() -> UIView { guard let footerModel = templateModel?.footer, let molecule = generateMoleculeView(from: footerModel) - else { return super.viewForBottom() } + else { + let view = super.viewForBottom() + view.backgroundColor = templateModel?.backgroundColor?.uiColor ?? .clear + return view + } return molecule } diff --git a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift index f480f083..4faff136 100644 --- a/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift +++ b/MVMCoreUI/BaseControllers/ThreeLayerTableViewController.swift @@ -55,7 +55,6 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController { createViewForTableHeader() createViewForTableFooter() tableView?.reloadData() - accessibilityElements = [tableView as Any] } override open func viewDidLoad() { @@ -169,6 +168,7 @@ open class ThreeLayerTableViewController: ProgrammaticTableViewController { bottomView = MVMCoreUICommonViewsUtility.getView(with: 0.5) } let footerView = MVMCoreUICommonViewsUtility.commonView() + footerView.backgroundColor = bottomView.backgroundColor footerView.addSubview(bottomView) bottomViewTopConstraint = bottomView.topAnchor.constraint(equalTo: footerView.topAnchor, constant: spaceAboveBottomView() ?? 0) bottomViewTopConstraint?.isActive = true diff --git a/MVMCoreUI/BaseControllers/ViewController.swift b/MVMCoreUI/BaseControllers/ViewController.swift index f148c1cb..d91e776e 100644 --- a/MVMCoreUI/BaseControllers/ViewController.swift +++ b/MVMCoreUI/BaseControllers/ViewController.swift @@ -196,7 +196,11 @@ import MVMCore return "Error parsing template. \((parsingError as NSError).localizedFailureReason ?? parsingError.localizedDescription)" } - open func parsePageJSON() throws { } + open func parsePageJSON() throws { + if let backgroundRequest = loadObject?.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject?.identifier { + MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageProcessingComplete(pageType: pageType, requestUUID: identifier, webUrl: nil)) + } + } open class func verifyRequiredModulesLoaded(for loadObject: MVMCoreLoadObject?, error: AutoreleasingUnsafeMutablePointer) -> Bool { guard let pageType = loadObject?.pageType, @@ -356,6 +360,10 @@ import MVMCore executeBehaviors { [weak self] (behavior: PageVisibilityBehavior) in behavior.onPageShown(self?.delegateObjectIVar) } + + if let backgroundRequest = loadObject?.requestParameters?.backgroundRequest, !backgroundRequest, let pageType, let identifier = loadObject?.identifier { + MVMCoreLoggingHandler.shared()?.logCoreEvent(.pageRenderComplete(pageType: pageType, requestUUID: identifier, templateName: loadObject?.pageJSON?.optionalStringForKey("template"), controllerName: "\(type(of: self))", error: loadObject?.responseInfoMap?.optionalStringForKey("message"))) + } } open override func viewWillAppear(_ animated: Bool) { @@ -427,7 +435,7 @@ import MVMCore // Open the support panel if error == nil, - loadObject?.requestParameters?.openSupportPanel ?? (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) == true { + (loadObject?.requestParameters?.openSupportPanel ?? false) || (loadObject?.systemParametersJSON?.boolForKey(KeyOpenSupport) ?? false) { MVMCoreUISession.sharedGlobal()?.splitViewController?.showRightPanel(animated: true) } } @@ -496,7 +504,7 @@ import MVMCore errorObject.silentError = false } - MVMCoreUIActionHandler.shared()?.defaultHandleActionError(errorObject, additionalData: additionalData) + MVMCoreUIActionHandler.shared()?.defaultHandleActionError(errorObject, additionalData: additionalData, delegateObject: delegateObject) } //-------------------------------------------------- diff --git a/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift new file mode 100644 index 00000000..78bb1129 --- /dev/null +++ b/MVMCoreUI/Behaviors/GetNotificationAuthStatusBehavior.swift @@ -0,0 +1,59 @@ +// +// GetNotificationAuthStatusBehavior.swift +// MVMCoreUI +// +// Created by Edayattu Salam, Nowfal on 13/04/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +public protocol GetNotificationAuthStatusBehaviorConsumerProtocol { + func consume(notificationStatus: UNAuthorizationStatus) +} + +public class GetNotificationAuthStatusBehaviorModel: PageBehaviorModelProtocol { + public class var identifier: String { "notificationAuthStatus" } + public var shouldAllowMultipleInstances: Bool { false } + + public init() { } +} + +public class GetNotificationAuthStatusBehavior: PageVisibilityBehavior { + var delegate: MVMCoreUIDelegateObject? + + public required init(model: PageBehaviorModelProtocol, delegateObject: MVMCoreUIDelegateObject?) { + self.delegate = delegateObject + } + + public func onPageShown(_ delegateObject: MVMCoreUIDelegateObject?) { + // Get notification auth status + getNotificationStatus() + + NotificationCenter.default.addObserver(self, selector: #selector(onForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + } + + @objc public func onForeground() { + // Get notification authorization status + getNotificationStatus() + } + + public func getNotificationStatus() { + guard let rootMolecules = self.delegate?.moleculeDelegate?.getRootMolecules() else { return } + let consumers: [GetNotificationAuthStatusBehaviorConsumerProtocol] = rootMolecules.allMoleculesOfType() + + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { (settings) in + for consumer in consumers { + consumer.consume(notificationStatus: settings.authorizationStatus) + } + // Tell template to update + MVMCoreDispatchUtility.performBlock(onMainThread: { + guard let controller = self.delegate?.moleculeDelegate as? ViewController else { return } + controller.handleNewDataAndUpdateUI() + }) + } + } + + public func onPageHidden(_ delegateObject: MVMCoreUIDelegateObject?) { + NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil) + } +} diff --git a/MVMCoreUI/Containers/NavigationController/NavigationController.swift b/MVMCoreUI/Containers/NavigationController/NavigationController.swift index eb43201a..8f72a792 100644 --- a/MVMCoreUI/Containers/NavigationController/NavigationController.swift +++ b/MVMCoreUI/Containers/NavigationController/NavigationController.swift @@ -7,9 +7,12 @@ // import UIKit +import MVMCore +import Combine @objcMembers open class NavigationController: UINavigationController, MVMCoreViewManagerViewControllerProtocol { public weak var manager: (UIViewController & MVMCoreViewManagerProtocol)? + private var cancellables: Set = [] /// Getter for the main navigation controller public static func navigationController() -> Self? { @@ -20,9 +23,9 @@ import UIKit public static func setupNavigationController() -> Self? { let navigationController = self.init() MVMCoreUISession.sharedGlobal()?.navigationController = navigationController - MVMCoreNavigationHandler.shared()?.viewControllerToPresentOn = navigationController - MVMCoreNavigationHandler.shared()?.navigationController = navigationController - MVMCoreNavigationHandler.shared()?.addDelegate(navigationController) + NavigationHandler.shared().viewControllerToPresentOn = navigationController + NavigationHandler.shared().navigationController = navigationController + navigationController.subscribe() navigationController.setNavigationBarUI(with: NavigationItemModel()) return navigationController } @@ -34,15 +37,45 @@ import UIKit return navigationController } - /// Convenience function to return the navigation model of the lowest controller traversing managers if applicable. + /** Subscribes for events. + Updates the navigation item of the new view controller when one is pushed. + Based on ``NavigationItemModelProtocol`` of ``PageProtocol/pageModel``. Traverses the manager for the view controller if necessary. + */ + @MainActor + public func subscribe() { + NavigationHandler.shared().onNavigation.sink { [weak self] (event, operation) in + guard let self = self, + self == operation.navigationType.getNavigationController(), + let viewController = NavigationHandler.shared().getViewControllers(for: self).last, + let newViewController = MVMCoreUIUtility.getViewControllerTraversingManagers(viewController) else { return } + switch event { + case .willNavigate: + if let controller = viewController as? (UIViewController & MVMCoreViewManagerViewControllerProtocol) { + MVMCoreViewManagerViewControllerProtocolHelper.helpSetManager(self, viewController: controller) + } + if let model = getNavigationModel(from: newViewController) { + self.setNavigationItem(with: model, for: viewController) + self.setNavigationBarUI(with: model) + } + self.manager?.willDisplay?(newViewController) + case .didNavigate: + self.manager?.displayedViewController?(newViewController) + if let controller = viewController as? (UIViewController & MVMCoreViewManagerViewControllerProtocol) { + controller.viewControllerReady?(inManager: self) + } + @unknown default: break + } + }.store(in: &cancellables) + } + + /// Convenience function to return the navigation model of the view controller. public func getNavigationModel(from viewController: UIViewController) -> NavigationItemModelProtocol? { return (viewController as? PageProtocol)?.pageModel?.navigationBar } /// Verifies the controller is the currently displayed controller. public func isDisplayed(viewController: UIViewController) -> Bool { - guard let topViewController = topViewController, - viewController == MVMCoreUIUtility.getViewControllerTraversingManagers(topViewController) else { + guard viewController == getViewController() else { return false } return true @@ -84,8 +117,10 @@ extension NavigationController: MVMCoreViewManagerProtocol { manager?.willDisplay?(viewController) } + /// Updates the navigation item/bar of the current view controller based on the passed in model and view controller. + @MainActor private func updateNavigationView(with model: NavigationItemModelProtocol, for viewController: UIViewController) { - guard let topViewController = topViewController else { return } + guard let topViewController = NavigationHandler.shared().getViewControllers(for: self).last else { return } setNavigationItem(with: model, for: topViewController, coordinatingWith: viewController as? PageBehaviorHandlerProtocol) setNavigationBarUI(with: model) @@ -99,34 +134,6 @@ extension NavigationController: MVMCoreViewManagerProtocol { } } -extension NavigationController: MVMCorePresentationDelegateProtocol { - public func navigationController(_ navigationController: UINavigationController, prepareDisplayFor viewController: UIViewController) { - if let controller = viewController as? (UIViewController & MVMCoreViewManagerViewControllerProtocol) { - MVMCoreViewManagerViewControllerProtocolHelper.helpSetManager(self, viewController: controller) - } - guard self == navigationController, - let newViewController = MVMCoreUIUtility.getViewControllerTraversingManagers(viewController), - let model = getNavigationModel(from: newViewController) else { return } - setNavigationItem(with: model, for: viewController) - setNavigationBarUI(with: model) - } - - public func navigationController(_ navigationController: UINavigationController, willDisplay viewController: UIViewController) { - guard self == navigationController, - let newViewController = MVMCoreUIUtility.getViewControllerTraversingManagers(viewController) else { return } - manager?.willDisplay?(newViewController) - } - - public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { - guard self == navigationController, - let newViewController = MVMCoreUIUtility.getViewControllerTraversingManagers(viewController) else { return } - manager?.displayedViewController?(newViewController) - if let controller = viewController as? (UIViewController & MVMCoreViewManagerViewControllerProtocol) { - controller.viewControllerReady?(inManager: self) - } - } -} - extension UIColor { func image(_ size: CGSize = CGSize(width: 1, height: 1)) -> UIImage { return UIGraphicsImageRenderer(size: size).image { rendererContext in diff --git a/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift b/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift index 9a0665b9..625f0a25 100644 --- a/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift +++ b/MVMCoreUI/Containers/NavigationController/UINavigationController+Extension.swift @@ -7,6 +7,7 @@ // import Foundation +import MVMCore public extension UINavigationController { @@ -38,7 +39,7 @@ public extension UINavigationController { if model.hidesSystemBackButton, model.alwaysShowBackButton != false { if let backButtonModel = model.backButton, - MVMCoreNavigationHandler.shared()?.getViewControllers(for: self)?.count ?? 0 > 1 || model.alwaysShowBackButton ?? false { + NavigationHandler.shared().getViewControllers(for: self).count > 1 || model.alwaysShowBackButton ?? false { leftItems.append(backButtonModel.createNavigationItemButton(delegateObject: delegate, additionalData: nil)) } if let leftItemModels = model.additionalLeftButtons { @@ -100,11 +101,24 @@ public extension UINavigationController { appearance.backgroundColor = backgroundColor appearance.titleTextAttributes.updateValue(tint, forKey: .foregroundColor) appearance.titlePositionAdjustment = model.titleOffset ?? .zero - appearance.shadowColor = model.line?.backgroundColor?.uiColor ?? .clear + if let type = model.line?.type, + type != .none, + let color = model.line?.backgroundColor { + appearance.shadowColor = color.uiColor + } else { + appearance.shadowColor = .clear + } appearance.shadowImage = getNavigationBarShadowImage(for: model)?.withRenderingMode(.alwaysTemplate) navigationBar.standardAppearance = appearance navigationBar.scrollEdgeAppearance = appearance setNavigationBarHidden(model.hidden, animated: true) } + + @MainActor + func getViewController() -> UIViewController? { + guard let topViewController = getViewControllers().last, + let viewController = MVMCoreUIUtility.getViewControllerTraversingManagers(topViewController) else { return nil } + return viewController + } } diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift index c6ec9382..20f4449c 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift @@ -8,11 +8,20 @@ import Foundation import UIKit +import Combine +import MVMCore + +/// Allows overrides of the status bar color and style. +public protocol StatusBarUI { + /// Returns the background color of the status bar view and the style of the status bar. + func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) +} // Navigation bar update functions public extension MVMCoreUISplitViewController { - /// Updates the state for various controls (navigation, tab, progress) for the controller. + /// Updates the state for various controls (top navigation controller item, tab, progress) for the controller. + @MainActor func updateState(with viewController: UIViewController) { guard let navigationController = navigationController, navigationController.isDisplayed(viewController: viewController) else { return } @@ -22,7 +31,9 @@ public extension MVMCoreUISplitViewController { } // MARK: - Progress Bar - /// Updates the progress bar based on the page json for the view controller. + /** Updates the progress bar based on the page json for the view controller. + Uses a string value between 0 and 100 from key progressPercent in the MVMCoreViewControllerProtocol.loadObject.pageJSON. + */ func updateProgressBar(for viewController: UIViewController) { guard let viewController = viewController as? MVMCoreViewControllerProtocol else { return } var progress: Float = 0.0 @@ -34,12 +45,16 @@ public extension MVMCoreUISplitViewController { } // MARK: - Tab Bar - /// Updates the tab bar based on the page json for the view controller. + /** Updates the tab bar based on the page json for the view controller. + For the index: checks the view controller's pageModel (``PageProtocol``) property ``TabPageModelProtocol/tabBarIndex``, else tabBarIndex in action map that led to this page, else the previous tab bar index of this page. + For hidden: checks the view controller's pageModel (``PageProtocol``) property ``TabPageModelProtocol/tabBarHidden``, else tabBarHidden in action map that led to this page, else it is visibile. + */ + @MainActor func updateTabBar(for viewController: UIViewController) { let mvmViewController = viewController as? MVMCoreViewControllerProtocol tabBar?.delegateObject = mvmViewController?.delegateObject?() as? MVMCoreUIDelegateObject - let navigationIndex = (MVMCoreNavigationHandler.shared()?.getViewControllers(for: navigationController)?.count ?? 1) - 1 + let navigationIndex = (navigationController != nil ? NavigationHandler.shared().getViewControllers(for: navigationController!).count : 1) - 1 // Set the highlighted index. In terms of priority, Page > Action > Previous. if let index = ((viewController as? PageProtocol)?.pageModel as? TabPageModelProtocol)?.tabBarIndex { @@ -72,10 +87,13 @@ public extension MVMCoreUISplitViewController { } // MARK: - Navigation Bar - /// Convenience function. Sets the navigation and split view properties for the view controller. Panel access is determined if view controller is a detail view protocol. + /** Convenience function. Sets the navigation and split view properties for the view controller. + Panel access is determined if view controller is a ``MVMCoreUIDetailViewProtocol`` + */ + @MainActor func setNavigationBar(for viewController: UIViewController, navigationController: UINavigationController, navigationItemModel: NavigationItemModelProtocol) { guard navigationController == self.navigationController, - viewController == getCurrentDetailViewController() else { + self.navigationController?.isDisplayed(viewController: viewController) == true else { /// Not the split view navigation controller, skip split functions. return } @@ -89,9 +107,13 @@ public extension MVMCoreUISplitViewController { setNavigationIconColor(navigationItemModel.tintColor.uiColor) } - /// Sets the left navigation items for the view controller based on model and splitview. + /** Sets the left navigation items for the top view controller based on the model and viewController. + Panel access is determined if view controller is a ``MVMCoreUIDetailViewProtocol`` + */ + @MainActor func setLeftNavigationButtons(navigationController: UINavigationController, navigationItemModel: NavigationItemModelProtocol?, viewController: UIViewController) { - guard let topViewController = navigationController.topViewController else { return } + let viewControllers = NavigationHandler.shared().getViewControllers(for: navigationController) + guard let topViewController = viewControllers.last else { return } var leftItems: [UIBarButtonItem] = [] let delegate = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject @@ -102,7 +124,7 @@ public extension MVMCoreUISplitViewController { if let forceBackButton = navigationItemModel?.alwaysShowBackButton { showBackButton = forceBackButton } else { - showBackButton = MVMCoreNavigationHandler.shared()?.getViewControllers(for: navigationController)?.count ?? 0 > 1 + showBackButton = viewControllers.count > 1 } if showBackButton { if let backButtonModel = navigationItemModel?.backButton { @@ -136,9 +158,12 @@ public extension MVMCoreUISplitViewController { topViewController.navigationItem.setLeftBarButtonItems(leftItems.count > 0 ? leftItems : nil, animated: !DisableAnimations.boolValue) } - /// Sets the right navigation items for the view controller based on model and splitview. + /** Sets the right navigation items for the top view controller based on the model and viewController. + Panel access is determined if view controller is a ``MVMCoreUIDetailViewProtocol`` + */ + @MainActor func setRightNavigationButtons(navigationController: UINavigationController, navigationItemModel: NavigationItemModelProtocol?, viewController: UIViewController) { - guard let topViewController = navigationController.topViewController else { return } + guard let topViewController = NavigationHandler.shared().getViewControllers(for: navigationController).last else { return } let delegate = (viewController as? MVMCoreViewControllerProtocol)?.delegateObject?() as? MVMCoreUIDelegateObject var rightItems: [UIBarButtonItem] = [] @@ -165,6 +190,9 @@ public extension MVMCoreUISplitViewController { topViewController.navigationItem.setRightBarButtonItems(rightItems.count > 0 ? rightItems : nil, animated: !DisableAnimations.boolValue) } + /** If the current detail view controller has a navigation model. + ``NavigationController/getNavigationModel(from:)`` from ``getCurrentDetailViewController()`` + */ @objc func navigationBarModelExists() -> Bool { // Legacy Navigation guard let currentViewController = getCurrentDetailViewController(), @@ -172,14 +200,17 @@ public extension MVMCoreUISplitViewController { return true } - /// Convenience function to update the navigation bar if the controller is the current lowest controller. - @objc func updateNavigationBarFor(viewController: UIViewController) { + /// Convenience function to update the navigation bar if the controller is the current detail controller. + @MainActor @objc + func updateNavigationBarFor(viewController: UIViewController) { guard let navigationController = navigationController, navigationController.isDisplayed(viewController: viewController), let model = navigationController.getNavigationModel(from: viewController) else { return } setNavigationBar(for: viewController, navigationController: navigationController, navigationItemModel: model) - guard !(topAlertView?.overridingStatusBar() ?? false) else { return } - setStatusBarForCurrentViewController() + Task { + guard (await NotificationHandler.shared()?.getCurrentNotification()?.0 as? StatusBarUI) == nil || topAlertView == nil else { return } + setStatusBarForCurrentViewController() + } } // MARK: - Status Bar @@ -191,9 +222,13 @@ public extension MVMCoreUISplitViewController { return .default } - /// Updates the status bar background color and style. - @objc func setStatusBarForCurrentViewController() { - let viewController = getCurrentViewController() as? MVMCoreUIDetailViewProtocol + /** Updates the status bar background color and style for the passed view controller. + The background color is fetched from ``MVMCoreUIDetailViewProtocol/defaultStatusBarBackgroundColor()``, else the current navigation bar background, else the current status bar background color. + The backgroundStytle is fetched from ``MVMCoreUIDetailViewProtocol/defaultStatusBarStyle()`` else ``getStatusBarStyle(for:)`` + */ + @MainActor + func setStatusBar(for viewController: UIViewController?) { + let viewController = viewController as? MVMCoreUIDetailViewProtocol let backgroundColor = viewController?.defaultStatusBarBackgroundColor?() ?? navigationController?.navigationBar.standardAppearance.backgroundColor ?? statusBarView?.backgroundColor @@ -203,6 +238,14 @@ public extension MVMCoreUISplitViewController { setStatusBarBackgroundColor(backgroundColor, style: style) } + + /** Updates the status bar background color and style for the current view controller. + See ``setStatusBar(for:)`` + */ + @MainActor + @objc func setStatusBarForCurrentViewController() { + setStatusBar(for: getCurrentViewController()) + } } extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol { @@ -223,3 +266,45 @@ extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol { updateState(with: viewController) } } + +@objc public extension MVMCoreUISplitViewController { + @objc func goBack() { + Task(priority: .userInitiated) { @MainActor in + if let viewController = getCurrentDetailViewController() as? MVMCoreUIDetailViewProtocol, + let backButtonPressed = viewController.backButtonPressed { + backButtonPressed() + } else { + await NavigationHandler.shared().popTopViewController() + } + } + } + + /// Subscribes for notification events. + @objc func subscribeForNotifications() { + guard cancellables == nil else { return } + var cancellables = Set() + + // Ensure the status bar background color and tint are proper for the notification. + NotificationHandler.shared()?.onNotificationWillShow.receive(on: DispatchQueue.main) + .sink { [weak self] (notification, model) in + guard let conformer = notification as? StatusBarUI else { return } + let statusBarUI = conformer.getStatusBarUI() + self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style) + }.store(in: &cancellables) + + NotificationHandler.shared()?.onNotificationUpdated.receive(on: DispatchQueue.main) + .sink { [weak self] (notification, model) in + guard let conformer = notification as? StatusBarUI else { return } + let statusBarUI = conformer.getStatusBarUI() + self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style) + }.store(in: &cancellables) + + // Ensure the status bar background color and tint are proper for the view controller. + NotificationHandler.shared()?.onNotificationDismissed.receive(on: DispatchQueue.main) + .sink { (notification, model) in + guard notification is StatusBarUI else { return } + MVMCoreUISplitViewController.main()?.setStatusBarForCurrentViewController() + }.store(in: &cancellables) + self.cancellables = cancellables + } +} diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h index 589c1cbb..a5c1577f 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h @@ -12,7 +12,6 @@ #import #import -@class MVMCoreUITopAlertView; @class MFViewController; @class NavigationController; @protocol TabBarProtocol; @@ -42,7 +41,7 @@ typedef NS_ENUM(NSInteger, MFNumberOfDrawers) { @property (nullable, weak, nonatomic, readonly) NavigationController *navigationController; // Reference to the top alert view -@property (nullable, weak, nonatomic) MVMCoreUITopAlertView *topAlertView; +@property (nullable, weak, nonatomic) UIView *topAlertView; // Reference to the status bar view @property (nullable, weak, nonatomic) UIView *statusBarView; @@ -58,6 +57,8 @@ typedef NS_ENUM(NSInteger, MFNumberOfDrawers) { /// Tab bar index history. Contains either indices (0, 1, etc) or NSNull if there was no tab bar indice to set. @property (nonnull, strong, nonatomic) NSMutableArray *tabBarIndices; +@property (nullable, strong, nonatomic) NSSet *cancellables; + // Convenience getter + (nullable instancetype)mainSplitViewController; @@ -65,10 +66,10 @@ typedef NS_ENUM(NSInteger, MFNumberOfDrawers) { - (nullable instancetype)initWithLeftPanel:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel; // Returns a split controller with the mvm styling. Also sets the appropriate handlers. -+ (nullable instancetype)setup:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel; ++ (nullable instancetype)setup:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel topAlertView:(nullable UIView*)topAlertView; // Returns a split controller with the mvm styling. Also sets the appropriate handlers. Also sets up the default load screen -+ (nullable instancetype)setupAsMainController:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel; ++ (nullable instancetype)setupAsMainController:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel topAlertView:(nullable UIView*)topAlertView; #pragma mark - Panel Functions diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m index 1ef5c232..c43d17d9 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m @@ -7,7 +7,6 @@ // #import "MVMCoreUISplitViewController.h" -@import MVMCore.MVMCoreNavigationHandler; @import MVMCore.MVMCoreDispatchUtility; @import MVMCore.MVMCoreViewManagerProtocol; @import MVMCore.MVMCoreActionUtility; @@ -20,8 +19,8 @@ #import "MVMCoreUISession.h" #import "MVMCoreUIConstants.h" #import "MVMCoreUICommonViewsUtility.h" -#import "MVMCoreUITopAlertView.h" #import +@import MVMCore.MVMCoreViewProtocol; @interface MVMCoreUISplitViewController () @@ -80,15 +79,22 @@ CGFloat const PanelAnimationDuration = 0.2; return [MVMCoreActionUtility initializerClassCheck:[MVMCoreUISession sharedGlobal].splitViewController classToVerify:self]; } -+ (nullable instancetype)setup:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel { ++ (nullable instancetype)setup:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel topAlertView:(nullable UIView*)topAlertView { MVMCoreUISplitViewController *splitViewController = [[self alloc] initWithLeftPanel:leftPanel rightPanel:rightPanel]; + splitViewController.topAlertView = topAlertView; [MVMCoreUISession sharedGlobal].splitViewController = splitViewController; + if (topAlertView) { + [splitViewController subscribeForNotifications]; + } return splitViewController; } -+ (nullable instancetype)setupAsMainController:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel { - MVMCoreUISplitViewController *splitViewController = [self setup:leftPanel rightPanel:rightPanel]; ++ (nullable instancetype)setupAsMainController:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel topAlertView:(nullable UIView*)topAlertView { + MVMCoreUISplitViewController *splitViewController = [self setup:leftPanel rightPanel:rightPanel topAlertView:topAlertView]; [[MVMCoreUISession sharedGlobal] setupAsStandardLoadViewDelegate:splitViewController]; + if (topAlertView) { + [splitViewController subscribeForNotifications]; + } return splitViewController; } @@ -185,14 +191,7 @@ CGFloat const PanelAnimationDuration = 0.2; } - (IBAction)backButtonPressed:(id)sender { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - UIViewController *detailViewController = [self getCurrentDetailViewController]; - if ([detailViewController conformsToProtocol:@protocol(MVMCoreUIDetailViewProtocol)] && [detailViewController respondsToSelector:@selector(backButtonPressed)]) { - [((UIViewController *)detailViewController) backButtonPressed]; - } else { - [[MVMCoreNavigationHandler sharedNavigationHandler] popTopViewControllerAnimated:YES]; - } - }]; + [self goBack]; } - (IBAction)rightPanelButtonPressed:(id)sender { @@ -251,7 +250,7 @@ CGFloat const PanelAnimationDuration = 0.2; - (void)setLeftNavigationItemForViewController:(UIViewController * _Nonnull)viewController accessible:(BOOL)accessible extended:(BOOL)extended { NSMutableArray *leftBarButtonItems = [NSMutableArray array]; - if (self.navigationController && [MVMCoreNavigationHandler.sharedNavigationHandler getViewControllersForNavigationController:self.navigationController].count > 1) { + if (self.navigationController && [self.navigationController getViewControllers].count > 1) { [leftBarButtonItems addObject:self.backButton]; } if ((accessible && !extended) && self.leftPanelButton) { @@ -904,14 +903,9 @@ CGFloat const PanelAnimationDuration = 0.2; self.statusBarView = statusBarView; // Top Alert - MVMCoreUITopAlertView *topAlertView = nil; - if ([[CoreUIObject sharedInstance].globalTopAlertDelegate respondsToSelector:@selector(getTopAlertView)]) { - topAlertView = (MVMCoreUITopAlertView *)[[CoreUIObject sharedInstance].globalTopAlertDelegate getTopAlertView]; - if (topAlertView) { - self.topAlertView = topAlertView; - [self.view addSubview:topAlertView]; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[topAlertView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"topAlertView":topAlertView}]]; - } + if (self.topAlertView) { + [self.view addSubview:self.topAlertView]; + [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-0-[topAlertView]-0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:@{@"topAlertView":self.topAlertView}]]; } // The main view. @@ -947,7 +941,8 @@ CGFloat const PanelAnimationDuration = 0.2; bottomProgressHeight.active = YES; self.bottomProgressBarHeightConstraint = bottomProgressHeight; - if (topAlertView) { + if (self.topAlertView) { + UIView *topAlertView = self.topAlertView; [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[statusBarView]-0-[topAlertView]-0-[mainView]-0-[progressView]" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(statusBarView,topAlertView, mainView, progressView)]]; } else { [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[statusBarView]-0-[mainView]-0-[progressView]" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(statusBarView,mainView, progressView)]]; @@ -1043,7 +1038,9 @@ CGFloat const PanelAnimationDuration = 0.2; /*self.leftPanel.needToUpdateUI = YES; [self.leftPanel.view setNeedsLayout]; [self.leftPanel.view layoutIfNeeded];*/ - [self.topAlertView updateView:size.width]; + if ([self.topAlertView conformsToProtocol:@protocol(MVMCoreViewProtocol)]) { + [((UIView *)self.topAlertView) updateView:size.width]; + } }]; } @@ -1069,7 +1066,7 @@ CGFloat const PanelAnimationDuration = 0.2; // Returns the desired view or falls back. Hot fix until we can get away from using these functions... + (CGRect)getBounds:(UIView *)desiredView { - UIView *view = desiredView ?: [MVMCoreNavigationHandler sharedNavigationHandler].navigationController.view ?: [MVMCoreGetterUtility getKeyWindow].rootViewController.view; + UIView *view = desiredView ?: [self mainSplitViewController].navigationController.view ?: [MVMCoreGetterUtility getKeyWindow].rootViewController.view; return view ? view.bounds : [UIScreen mainScreen].bounds; } @@ -1094,17 +1091,7 @@ CGFloat const PanelAnimationDuration = 0.2; } - (UIViewController *)getCurrentVisibleController { - UIViewController *baseViewController = [MVMCoreNavigationHandler sharedNavigationHandler].viewControllerToPresentOn ?: [MVMCoreGetterUtility getKeyWindow].rootViewController; - UIViewController *viewController = nil; - while (baseViewController.presentedViewController && !baseViewController.presentedViewController.isBeingDismissed) { - viewController = baseViewController.presentedViewController; - baseViewController = viewController; - } - // if it is not presented viewcontroller, existing BAU logic will be working - if (!viewController) { - viewController = [MVMCoreUIUtility getViewControllerTraversingManagers:self.navigationController.topViewController]; - } - return viewController; + return [MVMCoreUIUtility getCurrentVisibleController]; } - (UIViewController *)getCurrentDetailViewController { diff --git a/MVMCoreUI/Containers/Views/Container.swift b/MVMCoreUI/Containers/Views/Container.swift index 32c22b54..281f8d1b 100644 --- a/MVMCoreUI/Containers/Views/Container.swift +++ b/MVMCoreUI/Containers/Views/Container.swift @@ -31,6 +31,11 @@ open class Container: View, ContainerProtocol { guard let containerModel = model as? ContainerModelProtocol else { return } containerHelper.set(with: containerModel, for: view as? MVMCoreUIViewConstrainingProtocol) + if let cornerRadius = (containerModel as? ContainerModel)?.cornerRadius { + layer.cornerRadius = cornerRadius + } else { + layer.cornerRadius = 0 + } } override open func reset() { diff --git a/MVMCoreUI/Containers/Views/ContainerModel.swift b/MVMCoreUI/Containers/Views/ContainerModel.swift index caa949df..b54590dd 100644 --- a/MVMCoreUI/Containers/Views/ContainerModel.swift +++ b/MVMCoreUI/Containers/Views/ContainerModel.swift @@ -13,6 +13,8 @@ open class ContainerModel: ContainerModelProtocol, Codable { // MARK: - Properties //-------------------------------------------------- + public var id: String = UUID().uuidString + public var horizontalAlignment: UIStackView.Alignment? public var useHorizontalMargins: Bool? public var leftPadding: CGFloat? @@ -23,11 +25,14 @@ open class ContainerModel: ContainerModelProtocol, Codable { public var topPadding: CGFloat? public var bottomPadding: CGFloat? + public var cornerRadius: CGFloat? + //-------------------------------------------------- // MARK: - Keys //-------------------------------------------------- private enum CodingKeys: String, CodingKey { + case id case horizontalAlignment case useHorizontalMargins case leftPadding @@ -36,6 +41,7 @@ open class ContainerModel: ContainerModelProtocol, Codable { case useVerticalMargins case topPadding case bottomPadding + case cornerRadius } //-------------------------------------------------- @@ -71,6 +77,9 @@ open class ContainerModel: ContainerModelProtocol, Codable { required public init(from decoder: Decoder) throws { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) + + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString + if let verticalAlignmentString = try typeContainer.decodeIfPresent(String.self, forKey: .verticalAlignment) { verticalAlignment = ContainerHelper.getAlignment(for: verticalAlignmentString) } @@ -83,11 +92,13 @@ open class ContainerModel: ContainerModelProtocol, Codable { useVerticalMargins = try typeContainer.decodeIfPresent(Bool.self, forKey: .useVerticalMargins) topPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .topPadding) bottomPadding = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .bottomPadding) + cornerRadius = try typeContainer.decodeIfPresent(CGFloat.self, forKey: .cornerRadius) setDefaults() } open func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) try container.encodeIfPresent(ContainerHelper.getAlignmentString(for: verticalAlignment), forKey: .verticalAlignment) try container.encodeIfPresent(ContainerHelper.getAlignmentString(for: horizontalAlignment), forKey: .horizontalAlignment) try container.encodeIfPresent(useHorizontalMargins, forKey: .useHorizontalMargins) @@ -96,5 +107,6 @@ open class ContainerModel: ContainerModelProtocol, Codable { try container.encodeIfPresent(useVerticalMargins, forKey: .useVerticalMargins) try container.encodeIfPresent(topPadding, forKey: .topPadding) try container.encodeIfPresent(bottomPadding, forKey: .bottomPadding) + try container.encodeIfPresent(cornerRadius, forKey: .cornerRadius) } } diff --git a/MVMCoreUI/MVMCoreUI.h b/MVMCoreUI/MVMCoreUI.h index 37083fd0..fa2aaf0b 100644 --- a/MVMCoreUI/MVMCoreUI.h +++ b/MVMCoreUI/MVMCoreUI.h @@ -20,26 +20,6 @@ FOUNDATION_EXPORT const unsigned char MVMCoreUIVersionString[]; #import #import #import -#import - -// Alert Handling -#import -#import -#import -#import - -#pragma mark - TopAlert -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import #pragma mark - Categories #import diff --git a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift index db339cf8..458c0169 100644 --- a/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift +++ b/MVMCoreUI/Managers/SubNav/SubNavManagerController.swift @@ -205,7 +205,7 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, } } - public func navigationController(_ navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { + public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { // Only percent interact if we've already loaded the view controller guard let customInteractor = customInteractor, let index = index, @@ -229,7 +229,7 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, hideNavigationBarLine(true) } - public func navigationController(_ navigationController: UINavigationController, willDisplay viewController: UIViewController) { + public func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { guard navigationController == subNavigationController else { return } if let viewController = viewController as? UIViewController & MVMCoreViewManagerViewControllerProtocol & MVMCoreViewControllerProtocol { @@ -247,7 +247,7 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, commitTo(controller: viewController) } - public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { + public func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { guard navigationController == subNavigationController else { return } // Need to track swipe action. if needToTrackTabSelect { @@ -272,7 +272,9 @@ open class SubNavManagerController: ViewController, MVMCoreViewManagerProtocol, if let controller = viewControllers[indexPath.row] { // Load controller from the cache needToTrackTabSelect = true - MVMCoreNavigationHandler.shared()?.replaceTopViewController(with: controller, navigationController: subNavigationController, animated: true, delegate: self, replaceInStack: false, completionHandler: nil) + Task(priority: .userInitiated) { + await NavigationHandler.shared().replace(viewController: controller, navigationController:subNavigationController, delegateObject:delegateObject(), tryToReplace: false, animated: true) + } } else if let tabsModel = tabs.tabsModel, let action = tabsModel.tabs[indexPath.row].action { // Perform the tab action diff --git a/MVMCoreUI/Notification/NotificationContainerView.swift b/MVMCoreUI/Notification/NotificationContainerView.swift new file mode 100644 index 00000000..62d05a9d --- /dev/null +++ b/MVMCoreUI/Notification/NotificationContainerView.swift @@ -0,0 +1,102 @@ +// +// MVMCoreUITopAlertView+Extension.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 9/11/20. +// Copyright © 2020 Verizon Wireless. All rights reserved. +// + +import Foundation +import MVMCore + +/// A simple container view that shows and hides a notification. +public class NotificationContainerView: UIView { + + public var currentNotificationView: UIView? + + lazy private var height = heightAnchor.constraint(equalToConstant: 0) + + public init() { + super.init(frame: .zero) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + /// Posts a layout change with taking the arguments from the view following the AccessibilityProtocol. + private func updateAccessibilityForTopAlert(_ view: UIView) { + // Update accessibility with top alert + var accessibilityArgument: Any? = view + if let view = view as? AccessibilityProtocol { + accessibilityArgument = view.getAccessibilityLayoutChangedArgument() + } + UIAccessibility.post(notification: .layoutChanged, argument: accessibilityArgument) + } +} + +extension NotificationContainerView: NotificationTransitionDelegateProtocol { + @MainActor + public func show(notification: UIView) async { + currentNotificationView?.removeFromSuperview() + addSubview(notification) + NSLayoutConstraint.constraintPinSubview(toSuperview: notification) + currentNotificationView = notification + + if let conformer = notification as? MVMCoreViewProtocol { + conformer.updateView(bounds.width) + } + + superview?.layoutIfNeeded() + await withCheckedContinuation { continuation in + UIView.animate(withDuration: 0.5) { + self.height.isActive = false + self.superview?.layoutIfNeeded() + } completion: { finished in + self.superview?.layoutIfNeeded() + self.updateAccessibilityForTopAlert(notification) + continuation.resume() + } + } + } + + @MainActor + public func hide(notification: UIView) async { + // accessibility - below line added to notify VI user through voiceover user when the top alert is closed + UIAccessibility.post(notification: .screenChanged, argument: MVMCoreUIUtility.hardcodedString(withKey: "AccTopAlertClosed")) + await withCheckedContinuation { continuation in + UIView.animate(withDuration: 0.5) { + self.height.isActive = true + self.superview?.layoutIfNeeded() + } completion: { finished in + UIAccessibility.post(notification: .layoutChanged, argument: nil) + self.currentNotificationView?.removeFromSuperview() + self.currentNotificationView = nil + continuation.resume() + } + } + } + + @MainActor + public func update(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) { + guard let molecule = currentNotificationView as? MoleculeViewProtocol else { return } + // Update molecule + molecule.reset() + molecule.set(with: model.molecule, delegateObject, nil) + (molecule as? MVMCoreViewProtocol)?.updateView(self.bounds.width) + } +} + +extension NotificationContainerView: MVMCoreViewProtocol { + public func updateView(_ size: CGFloat) { + (currentNotificationView as? MVMCoreViewProtocol)?.updateView(size) + } + + public func setupView() { + translatesAutoresizingMaskIntoConstraints = false + clipsToBounds = true + height.isActive = true + } +} diff --git a/MVMCoreUI/Notification/NotificationHandler.swift b/MVMCoreUI/Notification/NotificationHandler.swift new file mode 100644 index 00000000..9ecc2efb --- /dev/null +++ b/MVMCoreUI/Notification/NotificationHandler.swift @@ -0,0 +1,588 @@ +// +// TopNotificationHandler.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 4/11/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import MVMCore +import Dispatch +import Combine + +/// Handles the UI tasks for the notification. +public protocol NotificationTransitionDelegateProtocol { + @MainActor + func show(notification: UIView) async + + @MainActor + func hide(notification: UIView) async + + @MainActor + func update(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) +} + +/// An operation for managing the life cycle of the notification. +public class NotificationOperation: MVMCoreOperation { + + public let notification: UIView + + public var notificationModel: NotificationModel + + /// The delegate that manages transitioning the notification. + private let transitionDelegate: NotificationTransitionDelegateProtocol + + /// The showing animation transition operation. + private weak var showTransitionOperation: Operation? + + /// The stop timer for non-persistent notifications. + private var timerSource: DispatchSourceTimer? + + /// Determines if the operation is ready. For example, certain notifications are only meant to be displayed on certain pages and this can be set accordingly. + public var isDisplayable: Bool { + get { + var isDisplayable: Bool = true + // TODO: Causes crash. Check thoughts of reviewer. + //displayableQueue.sync { + isDisplayable = _isDisplayable + //} + return isDisplayable + } + set { + guard newValue != isDisplayable else { return } + //displayableQueue.async(flags: .barrier) { [weak self] in + self._isDisplayable = newValue + //} + } + } + /// Thread safety. + private var displayableQueue = DispatchQueue(label: "displayable", attributes: .concurrent) + /// Updates the operation readiness accordingly. + private var _isDisplayable: Bool = true { + willSet { + guard super.isReady else { return } + willChangeValue(for: \.isReady) + } + didSet { + guard super.isReady else { return } + didChangeValue(for: \.isReady) + } + } + + /// This operation is ready only if this notification is allowed to show. + public override var isReady: Bool { + get { + guard !isCancelled else { return super.isReady } + return super.isReady && isDisplayable + } + } + + public actor Properties { + /// If the notification is currently displayed. + private var isDisplayed: Bool = false + + /// If the notification is currently animating (showing/hiding). + private var isAnimating: Bool = false + + fileprivate func set(displayed: Bool) { + isDisplayed = displayed + } + + public func getIsDisplayed() -> Bool { + return isDisplayed + } + + fileprivate func set(animating: Bool) { + isAnimating = animating + } + + public func getIsAnimating() -> Bool { + return isAnimating + } + } + /// Actor isolated properties for the operation. + public var properties = Properties() + + /// A flag for tracking if the operation needs to be re-added because it was cancelled for a higher priority notification. + public var reAddAfterCancel = false + + public init(with notification: UIView, notificationModel: NotificationModel, transitionDelegate: NotificationTransitionDelegateProtocol) { + self.notification = notification + self.notificationModel = notificationModel + self.transitionDelegate = transitionDelegate + super.init() + queuePriority = notificationModel.priority + } + + public override func main() { + log(message: "Operation Started") + guard !checkAndHandleForCancellation() else { return } + Task { + await withCheckedContinuation { continuation in + // Show the notification. + showTransitionOperation = add(transition: { [weak self] in + guard let self = self else { return } + NotificationHandler.shared()?.onNotificationWillShow.send((self.notification, self.notificationModel)) + self.log(message: "Operation Will Show") + await self.showNotification() + }, completionBlock: { + continuation.resume() + }) + } + guard await properties.getIsDisplayed() else { + // If the animation did not complete... + log(message: "Operation Never Shown") + markAsFinished() + return + } + + // Publish that the notification has been shown. + NotificationHandler.shared()?.onNotificationShown.send((notification, notificationModel)) + log(message: "Operation Did Show") + + guard !isCancelled else { + // If cancelled during the animation, dismiss immediately. + stop() + return + } + updateStopTimer() + } + } + + public func stop() { + if let timerSource = timerSource { + timerSource.cancel() + } + Task { + guard await properties.getIsDisplayed(), + await !properties.getIsAnimating() else { return } + // Hide the notification + await withCheckedContinuation({ continuation in + _ = add(transition: { [weak self] in + guard let self = self else { return } + self.log(message: "Operation Will Dismiss") + NotificationHandler.shared()?.onNotificationWillDismiss.send((notification, notificationModel)) + await self.hideNotification() + }, completionBlock: { + continuation.resume() + }) + }) + // The animation must complete... + guard await !self.properties.getIsDisplayed() else { return } + + // Publish that the notification has been hidden. + NotificationHandler.shared()?.onNotificationDismissed.send((notification, notificationModel)) + log(message: "Operation Did Dismiss") + + guard isCancelled || !notificationModel.persistent else { return } + markAsFinished() + } + } + + public override func markAsFinished() { + log(message: "Operation Finished") + super.markAsFinished() + } + + public override func cancel() { + super.cancel() + log(message: "Operation Cancelled") + Task { + if await !properties.getIsDisplayed() { + // Cancel any pending show transitions. + showTransitionOperation?.cancel() + } + + // Do nothing if animating. + guard await !properties.getIsAnimating() else { return } + if await properties.getIsDisplayed() { + stop() + } else if isExecuting { + markAsFinished() + } + } + } + + public func log(message: String) { + MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "------Notification message: \(message) type: \(notificationModel.type) title: \(String(describing: (notificationModel.molecule as? NotificationMoleculeModel)?.headline.text)) id: \(notificationModel.id) priority: e\(notificationModel.priority.rawValue) a\(queuePriority.rawValue) operation: \(String(describing: self)) ------") + } + + // MARK: - Automatic + + /// Sets up a timer to hide the notification. + private func updateStopTimer() { + if let timerSource = timerSource { + timerSource.cancel() + } + guard !notificationModel.persistent else { return } + timerSource = DispatchSource.makeTimerSource() + timerSource?.setEventHandler(handler: { [weak self] in + self?.log(message: "Operation Time Out") + guard let self = self, + !self.isFinished, + !self.checkAndHandleForCancellation() else { return } + + // If voice over is on and the notification is focused, do not collapse until unfocused. + guard !MVMCoreUIUtility.viewContainsAccessiblityFocus(notification) else { + NotificationCenter.default.addObserver(self, selector: #selector(accessibilityFocusChanged), name: UIAccessibility.elementFocusedNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(accessibilityFocusChanged), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil) + return + } + self.stop() + }) + timerSource?.setCancelHandler(handler: { [weak self] in + self?.log(message: "Operation Time Out Cancel") + }) + timerSource?.schedule(deadline: .now() + .seconds(notificationModel.dismissTime)) + timerSource?.activate() + } + + /// If the voice over user leaves top alert focus, hide. + @objc func accessibilityFocusChanged(_ notification: NSNotification) { + guard UIAccessibility.isVoiceOverRunning else { + accessibilityFocusFinished() + return + } + guard let _ = notification.userInfo?[UIAccessibility.focusedElementUserInfoKey], + !MVMCoreUIUtility.viewContainsAccessiblityFocus(self.notification) else { return } + accessibilityFocusFinished() + } + + /// Dismisses the nofitication when we lose focus. + private func accessibilityFocusFinished() { + log(message: "Operation Accessibility Focus Removed") + NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil) + stop() + } + + // MARK: - Transitions + + /// Adds the transition of the notification to the navigation queue to avoid animation collisions. + private func add(transition: @escaping () async -> Void, completionBlock: (() -> Void)?) -> Operation { + let transitionOperation = MVMCoreBlockOperation(block: { blockOperation in + guard !blockOperation.checkAndHandleForCancellation() else { return } + Task { + await transition() + blockOperation.markAsFinished() + } + })! + transitionOperation.completionBlock = completionBlock + NavigationHandler.shared().navigationQueue.addOperation(transitionOperation) + return transitionOperation + } + + @MainActor + private func showNotification() async { + await properties.set(animating: true) + await transitionDelegate.show(notification: notification) + await properties.set(displayed: true) + await properties.set(animating: false) + } + + @MainActor + private func hideNotification() async { + await properties.set(animating: true) + await transitionDelegate.hide(notification: notification) + await properties.set(displayed: false) + await properties.set(animating: false) + } +} + +extension NotificationOperation: NSCopying { + public func copy(with zone: NSZone? = nil) -> Any { + let operation = NotificationOperation(with: notification, notificationModel: notificationModel, transitionDelegate: transitionDelegate) + operation.reAddAfterCancel = reAddAfterCancel + operation.isDisplayable = isDisplayable + for dependency in dependencies { + operation.addDependency(dependency) + } + operation.name = name + operation.qualityOfService = qualityOfService + return operation + } +} + +/// Manages notifications. +open class NotificationHandler { + + private var queue: OperationQueue = { + let queue = OperationQueue() + queue.maxConcurrentOperationCount = 1 + return queue + }() + + private var transitionDelegate: NotificationTransitionDelegateProtocol + + private var cancellable: Cancellable? + + // MARK: - Publishers + + /// Publishes when a notification will show. + public let onNotificationWillShow = PassthroughSubject<(UIView, NotificationModel), Never>() + + /// Publishes when a notification is shown. + public let onNotificationShown = PassthroughSubject<(UIView, NotificationModel), Never>() + + /// Publishes when a notification will dismiss. + public let onNotificationWillDismiss = PassthroughSubject<(UIView, NotificationModel), Never>() + + /// Publishes when a notification is dismissed. + public let onNotificationDismissed = PassthroughSubject<(UIView, NotificationModel), Never>() + + /// Publishes when a notification is updated. + public let onNotificationUpdated = PassthroughSubject<(UIView, NotificationModel), Never>() + + // MARK: - + + /// Returns the handler stored in the CoreUIObject + public static func shared() -> Self? { + guard let shared = CoreUIObject.sharedInstance()?.topNotificationHandler else { return nil } + return MVMCoreActionUtility.fatalClassCheck(object: shared) + } + + public init(with transitionDelegate: NotificationTransitionDelegateProtocol) { + self.transitionDelegate = transitionDelegate + registerWithNotificationCenter() + registerForPageChanges() + } + + // MARK: - JSON Handling + + /// Registers with the notification center to know when json is updated. + private func registerWithNotificationCenter() { + NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) + } + + /// Registers to know when pages change. + private func registerForPageChanges() { + cancellable = NavigationHandler.shared().onNavigation + .filter({ $0.0 == .didNavigate }) + .sink { [weak self] (event, operation) in + guard let self = self else { return } + Task { + // Update displayable for each top alert operation when page type changes, in top queue priority order. + guard self.queue.operations.count > 0, + let navigationController = await operation.navigationType.getNavigationController(), + await navigationController == MVMCoreUISplitViewController.main()?.navigationController, + let viewController = await navigationController.getViewController() else { return } + let pageType = (viewController as? MVMCoreViewControllerProtocol)?.pageType + self.queue.operations.compactMap { + $0 as? NotificationOperation + }.sorted { + $0.notificationModel.priority.rawValue > $1.notificationModel.priority.rawValue + }.forEach { + $0.updateDisplayable(by: pageType) + } + self.reevaluteQueue() + } + } + } + + /// Checks for new top alert json + @objc private func responseJSONUpdated(notification: Notification) { + guard let loadObject = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject) else { return } + let delegateObject = loadObject.delegateObject as? MVMCoreUIDelegateObject + + // Dismiss any top alerts that server wants us to dismiss. + if let disableType = loadObject.responseInfoMap?.optionalStringForKey("disableType") { + NotificationHandler.shared()?.cancelNotification(using: { view, model in + return model.type == disableType + }) + } + + // Show any new top alert. + guard let responseJSON = loadObject.responseJSON, + let json = responseJSON.optionalDictionaryForKey(KeyTopAlert) else { return } + Task { + do { + try await showNotification(for: json, delegateObject: delegateObject) + } catch { + if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") { + MVMCoreUILoggingHandler.addError(toLog: errorObject) + } + } + } + } + + /// Converts the json to a model and creates the view and queues up the notification. + open func showNotification(for json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) async throws { + let model = try NotificationModel.decode(json: json, delegateObject: delegateObject) + try await showNotification(for: model, delegateObject: delegateObject) + } + + // MARK: - Operation Handling + + /// Adds the operation to the queue. + private func add(operation: NotificationOperation) { + operation.completionBlock = { [weak self] in + // If the alert was cancelled to show another with higher priority, re-add to the operation when cancelled to the queue. + if operation.reAddAfterCancel { + let newOperation: NotificationOperation = operation.copy() as! NotificationOperation + newOperation.reAddAfterCancel = false + self?.add(operation: newOperation) + } + operation.completionBlock = nil + } + + let currentPageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType + operation.updateDisplayable(by: currentPageType) + + queue.addOperation(operation) + operation.log(message: "Operation Added") + reevaluteQueue() + } + + /// Checks for existing top alert object of same type and updates it. Returns true if we updated. + open func checkAndUpdateExisting(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool { + for case let operation as NotificationOperation in queue.operations { + guard operation.notificationModel.type == model.type else { continue } + operation.update(with: model, delegateObject: delegateObject) + let pageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType + operation.updateDisplayable(by: pageType) + reevaluteQueue() + return true + } + return false + } + + /// Re-evaluates the queue operations + private func reevaluteQueue() { + var highestReadyOperation: NotificationOperation? + var executingOperation: NotificationOperation? + for case let operation as NotificationOperation in queue.operations { + guard !operation.isCancelled, + !operation.isFinished else { continue } + if operation.isReady, + highestReadyOperation == nil || operation.notificationModel.priority.rawValue > highestReadyOperation!.notificationModel.priority.rawValue { + highestReadyOperation = operation + } + if operation.isExecuting { + executingOperation = operation + } + } + guard let currentOperation = executingOperation else { return } + + // Cancel the executing operation if it is no longer ready to run. Re-add for later if it is persistent. + guard currentOperation.isReady else { + currentOperation.reAddAfterCancel = currentOperation.notificationModel.persistent + currentOperation.cancel() + return + } + + // If the highest priority operation is not executing, and the executing operation is persistent, cancel it. + if let newOperation = highestReadyOperation, + currentOperation != newOperation, + currentOperation.notificationModel.persistent { + currentOperation.reAddAfterCancel = true + currentOperation.cancel() + } + } + + // MARK: - Verify + + /// Returns if any notification is executing + open func isNotificationShowing() -> Bool { + return queue.operations.first(where: { operation in + return operation.isExecuting + }) != nil + } + + /** Returns if the first executing operation matches the provided predicate. + * @param predicate The predicate block to decide if it is the notification. + */ + open func hasNotification(using predicate: ((UIView, NotificationModel) -> Bool)) -> Bool { + return queue.operations.first(where: { operation in + guard operation.isExecuting, + let operation = operation as? NotificationOperation else { return false } + return predicate(operation.notification, operation.notificationModel) + }) as? NotificationOperation != nil + } + + /// Returns the current executing notification view and model + open func getCurrentNotification() async -> (UIView, NotificationModel)? { + for operation in queue.operations { + guard operation.isExecuting, + let operation = operation as? NotificationOperation, + await operation.properties.getIsDisplayed() else { continue } + return (operation.notification, operation.notificationModel) + } + return nil + } + + // MARK: - Show and hide + + /// Creates the view and queues up the notification. + open func showNotification(for model: NotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) async throws { + guard !checkAndUpdateExisting(with: model, delegateObject: delegateObject) else { return } + let view = try await createNotification(with: model, delegateObject: delegateObject) + let operation = NotificationOperation(with: view, notificationModel: model, transitionDelegate: transitionDelegate) + add(operation: operation) + } + + @MainActor + private func createNotification(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) async throws -> UIView & MoleculeViewProtocol { + guard let view = ModelRegistry.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else { + throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped") + } + return view + } + + /// Cancel the current top alert view. + open func hideNotification() { + guard let currentOperation = queue.operations.first(where: { operation in + return operation.isExecuting && !operation.isCancelled + }) as? NotificationOperation else { return } + currentOperation.notificationModel.persistent = false + currentOperation.reAddAfterCancel = false + currentOperation.cancel() + } + + /** Iterates through all scheduled notifications and cancels any that match the provided predicate. + * @param predicate The predicate block to decide whether to cancel an notification. + */ + open func cancelNotification(using predicate: ((UIView, NotificationModel) -> Bool)) { + for case let operation as NotificationOperation in queue.operations { + if predicate(operation.notification, operation.notificationModel) { + operation.reAddAfterCancel = false + operation.cancel() + } + } + } + + /// Cancel all notifications, current or pending. + open func removeAllNotifications() { + queue.cancelAllOperations() + } +} + +extension NotificationOperation { + /// Updates the operation and notification with the new model. + public func update(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) { + self.notificationModel = model + queuePriority = model.priority + guard isExecuting, + !isCancelled else { return } + self.log(message: "Operation Updated") + displayableQueue.async(flags: .barrier) { [self] in + updateStopTimer() + } + Task { + await transitionDelegate.update(with: notificationModel, delegateObject: delegateObject) + NotificationHandler.shared()?.onNotificationUpdated.send((notification, notificationModel)) + } + } + + /// Updates if the operation is displayable based on the page type. + func updateDisplayable(by pageType: String?) { + guard let pages = notificationModel.pages else { + isDisplayable = true + return + } + guard let pageType = pageType else { + isDisplayable = false + return + } + isDisplayable = pages.contains(pageType) + } +} diff --git a/MVMCoreUI/TopAlert/TopNotificationModel.swift b/MVMCoreUI/Notification/NotificationModel.swift similarity index 85% rename from MVMCoreUI/TopAlert/TopNotificationModel.swift rename to MVMCoreUI/Notification/NotificationModel.swift index 75300a3f..435d95b6 100644 --- a/MVMCoreUI/TopAlert/TopNotificationModel.swift +++ b/MVMCoreUI/Notification/NotificationModel.swift @@ -1,5 +1,5 @@ // -// TopNotification.swift +// NotificationModel.swift // MVMCoreUI // // Created by Scott Pfeil on 9/11/20. @@ -7,8 +7,9 @@ // import Foundation +import MVMCore -open class TopNotificationModel: Codable { +open class NotificationModel: Codable, Identifiable { public var type: String public var priority = Operation.QueuePriority.normal public var molecule: MoleculeModelProtocol @@ -16,7 +17,8 @@ open class TopNotificationModel: Codable { public var dismissTime = 5 public var pages: [String]? public var analyticsData: JSONValueDictionary? - + public var id: String + private enum CodingKeys: String, CodingKey { case type case priority @@ -25,6 +27,7 @@ open class TopNotificationModel: Codable { case dismissTime case pages case analyticsData + case id } //-------------------------------------------------- @@ -55,17 +58,6 @@ open class TopNotificationModel: Codable { return (shifted / Float(scale)) * 100.0 } - open func createTopAlertObject() -> MVMCoreTopAlertObject { - let object = MVMCoreTopAlertObject() - object.persistent = persistent - object.type = type - object.topAlertDismissTime = dismissTime - object.queuePriority = priority - object.useNewStyle = true - object.json = toJSON() - return object - } - /// Decodes the top alert json to a model. public static func decode(json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) throws -> Self { let data = try JSONSerialization.data(withJSONObject: json) @@ -77,14 +69,17 @@ open class TopNotificationModel: Codable { // MARK: - Initializer //-------------------------------------------------- - public init(with type: String, molecule: MoleculeModelProtocol, priority: Operation.QueuePriority = .normal, persistent: Bool = false, dismissTime: Int = 5, pages: [String]? = nil, analyticsData: JSONValueDictionary? = nil) { + required public init(with type: String, molecule: MoleculeModelProtocol, priority: Operation.QueuePriority = .normal, persistent: Bool = false, dismissTime: Int? = nil, pages: [String]? = nil, analyticsData: JSONValueDictionary? = nil, id: String = UUID().uuidString) { self.type = type self.molecule = molecule self.priority = priority self.persistent = persistent - self.dismissTime = dismissTime + if let dismissTime = dismissTime { + self.dismissTime = dismissTime + } self.pages = pages self.analyticsData = analyticsData + self.id = id } //-------------------------------------------------- @@ -95,6 +90,7 @@ open class TopNotificationModel: Codable { let typeContainer = try decoder.container(keyedBy: CodingKeys.self) type = try typeContainer.decode(String.self, forKey: .type) molecule = try typeContainer.decodeModel(codingKey: .molecule) + id = try typeContainer.decodeIfPresent(String.self, forKey: .id) ?? UUID().uuidString if let priorityPercent = try typeContainer.decodeIfPresent(Float.self, forKey: .priority) { setPriority(with: priorityPercent) } @@ -117,5 +113,6 @@ open class TopNotificationModel: Codable { try container.encode(dismissTime, forKey: .dismissTime) try container.encodeIfPresent(pages, forKey: .pages) try container.encodeIfPresent(analyticsData, forKey: .analyticsData) + try container.encode(id, forKey: .id) } } diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index f020dcf7..b339cf94 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -157,6 +157,7 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: ListRightVariableToggleAllTextAndLinks.self, for: ListRightVariableToggleAllTextAndLinksModel.self) ModelRegistry.register(handler: ListRightVariableRightCaretAllTextAndLinks.self, for: ListRightVariableRightCaretAllTextAndLinksModel.self) ModelRegistry.register(handler: ListOneColumnFullWidthTextAllTextAndLinks.self, for: ListOneColumnFullWidthTextAllTextAndLinksModel.self) + ModelRegistry.register(handler: ListOneColumnFullWidthTextAllTextAndLinks.self, for: ListNotificationAuthModel.self) ModelRegistry.register(handler: ListOneColumnFullWidthTextBodyText.self, for: ListOneColumnFullWidthTextBodyTextModel.self) ModelRegistry.register(handler: ListTwoColumnCompareChanges.self, for: ListTwoColumnCompareChangesModel.self) ModelRegistry.register(handler: ListTwoColumnPriceDetails.self, for: ListTwoColumnPriceDetailsModel.self) @@ -209,7 +210,7 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: TitleLockup.self, for: TitleLockupModel.self) // MARK: - Top Notifications - ModelRegistry.register(handler: NotificationView.self, for: NotificationModel.self) + ModelRegistry.register(handler: NotificationMoleculeView.self, for: NotificationMoleculeModel.self) ModelRegistry.register(handler: CollapsableNotification.self, for: CollapsableNotificationModel.self) } @@ -226,13 +227,12 @@ open class CoreUIModelMapping: ModelMapping { ModelRegistry.register(handler: ScreenBrightnessModifierBehavior.self, for: ScreenBrightnessModifierBehaviorModel.self) ModelRegistry.register(handler: PageGetContactBehavior.self, for: PageGetContactBehaviorModel.self) ModelRegistry.register(handler: AddRemoveMoleculesBehavior.self, for: AddRemoveMoleculesBehaviorModel.self) + ModelRegistry.register(handler: GetNotificationAuthStatusBehavior.self, for: GetNotificationAuthStatusBehaviorModel.self) } open override class func registerActions() { super.registerActions() - ModelRegistry.register(handler: ActionPopupHandler.self, for: ActionPopupModel.self) ModelRegistry.register(handler: ActionAlertHandler.self, for: ActionAlertModel.self) - ModelRegistry.register(handler: ActionTopAlertHandler.self, for: ActionTopAlertModel.self) ModelRegistry.register(handler: ActionCollapseNotificationHandler.self, for: ActionCollapseNotificationModel.self) ModelRegistry.register(handler: ActionDismissNotificationHandler.self, for: ActionDismissNotificationModel.self) ModelRegistry.register(handler: ActionOpenPanelHandler.self, for: ActionOpenPanelModel.self) diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index d44b49ac..b96e9b93 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -7,18 +7,21 @@ // import UIKit +import MVMCore @objcMembers open class CoreUIObject: MVMCoreObject { - public var globalTopAlertDelegate: MVMCoreGlobalTopAlertDelegateProtocol? + public var alertHandler: AlertHandler? + public var topNotificationHandler: NotificationHandler? open override func defaultInitialSetup() { CoreUIModelMapping.registerObjects() loadHandler = MVMCoreLoadHandler() cache = MVMCoreCache() + session = MVMCoreUISession() sessionHandler = MVMCoreSessionTimeHandler() actionHandler = MVMCoreUIActionHandler() - session = MVMCoreUISession() viewControllerMapping = MVMCoreUIViewControllerMappingObject() loggingDelegate = MVMCoreUILoggingHandler() + alertHandler = AlertHandler() } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIActionDelegateProtocol.h b/MVMCoreUI/OtherHandlers/MVMCoreUIActionDelegateProtocol.h deleted file mode 100644 index 41533f34..00000000 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIActionDelegateProtocol.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// MVMCoreUIActionDelegateProtocol.h -// MVMCoreUI -// -// Created by Scott Pfeil on 10/28/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// -@import MVMCore.MVMCoreActionDelegateProtocol; -@class MVMCoreAlertObject; - -@protocol MVMCoreUIActionDelegateProtocol - -// Gives the delegate a chance to alter the alert object -- (void)willShowPopupWithAlertObject:(nonnull MVMCoreAlertObject *)alertObject alertJson:(nonnull NSDictionary *)alertJson; - -// Gives the delegate a chance to alter the alert object -- (nullable MVMCoreAlertObject *)willShowTopAlertWithAlertObject:(nonnull MVMCoreAlertObject *)alertObject alertJson:(nonnull NSDictionary *)alertJson; - -@end diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.swift b/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.swift index 3de8fdde..49a3d5e9 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.swift +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIActionHandler.swift @@ -38,13 +38,13 @@ import SafariServices } /// Logs the error and shows a popup if the error is not silent. - open override func defaultHandleActionError(_ error: MVMCoreErrorObject, additionalData: [AnyHashable : Any]?) { + open override func defaultHandleActionError(_ error: MVMCoreErrorObject, additionalData: [AnyHashable : Any]?, delegateObject: DelegateObject? = nil) { super.defaultHandleActionError(error, additionalData: additionalData) guard !error.silentError else { return } error.silentError = true // Silence if this error is triggered again. (Legacy action handler flow.) Task(priority: .userInitiated) { @MainActor in - let alertObject = MVMCoreAlertObject.init(popupAlertWithError: error, isGreedy: false)! - MVMCoreAlertHandler.shared()?.showAlert(with: alertObject) + let alertAction = ActionAlertModel(alert: AlertModel(title: error.title ?? "", message: "\(String(describing: error.messageToDisplay)) (\(error.stringErrorCode()))", buttonModels: [AlertButtonModel(MVMCoreGetterUtility.hardcodedString(withKey: HardcodedOK) ?? "OK", ActionCancelModel())], delegateObject: delegateObject)) + try? await MVMCoreUIActionHandler.shared()?.handleAction(with: alertAction, additionalData: additionalData, delegateObject: delegateObject) } } @@ -57,6 +57,8 @@ import SafariServices @MainActor open func openURL(inSafariWebView url: URL) { let safariViewController = SFSafariViewController(url: url) - MVMCoreNavigationHandler.shared()?.present(safariViewController, animated: true) + Task(priority: .high) { + await NavigationHandler.shared().present(viewController: safariViewController) + } } } diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIDelegateObject.swift b/MVMCoreUI/OtherHandlers/MVMCoreUIDelegateObject.swift index b4ea0cd7..17ded151 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIDelegateObject.swift +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIDelegateObject.swift @@ -7,7 +7,7 @@ // import UIKit - +import MVMCore open class MVMCoreUIDelegateObject: DelegateObject { @@ -17,8 +17,7 @@ open class MVMCoreUIDelegateObject: DelegateObject { public weak var uiTextViewDelegate: UITextViewDelegate? public weak var observingTextFieldDelegate: ObservingTextFieldDelegate? public weak var moleculeDelegate: MoleculeDelegateProtocol? - public weak var alertDelegate: (MVMCoreAlertDelegateProtocol & NSObjectProtocol)? - public weak var topAlertDelegate: (MVMCoreTopAlertDelegateProtocol & NSObjectProtocol)? + public weak var alertDelegate: (AlertDelegateProtocol & NSObjectProtocol)? open override func setAll(withDelegate delegate: Any) { super.setAll(withDelegate: delegate) @@ -28,8 +27,7 @@ open class MVMCoreUIDelegateObject: DelegateObject { uiTextViewDelegate = delegate as? UITextViewDelegate observingTextFieldDelegate = delegate as? ObservingTextFieldDelegate moleculeDelegate = delegate as? MoleculeDelegateProtocol - alertDelegate = delegate as? (MVMCoreAlertDelegateProtocol & NSObjectProtocol) - topAlertDelegate = delegate as? (MVMCoreTopAlertDelegateProtocol & NSObjectProtocol) + alertDelegate = delegate as? (AlertDelegateProtocol & NSObjectProtocol) } class func delegateObject(from controller: MVMCoreViewControllerProtocol?) -> MVMCoreUIDelegateObject? { diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUILoggingDelegateProtocol.swift b/MVMCoreUI/OtherHandlers/MVMCoreUILoggingDelegateProtocol.swift new file mode 100644 index 00000000..3f135805 --- /dev/null +++ b/MVMCoreUI/OtherHandlers/MVMCoreUILoggingDelegateProtocol.swift @@ -0,0 +1,13 @@ +// +// MVMCoreUILoggingDelegateProtocol.swift +// MVMCoreUI +// +// Created by Scott Pfeil on 4/13/23. +// Copyright © 2023 Verizon Wireless. All rights reserved. +// + +import MVMCore + +public protocol MVMCoreUILoggingDelegateProtocol: MVMCoreLoggingDelegateProtocol { + func logAlert(with alertObject: AlertObject) +} diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.h b/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.h index b61c2b0a..a4b66ef9 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.h +++ b/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.h @@ -7,8 +7,6 @@ // @import MVMCore.MVMCoreLoggingHandler; -@class MFViewController; -@class MVMCoreTopAlertObject; NS_ASSUME_NONNULL_BEGIN @@ -21,9 +19,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)defaultLogActionForController:(nullable id )controller actionInformation:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData; - (nullable NSDictionary *)defaultGetActionTrackDataDictionaryForController:(nullable id )controller actionInformation:(nullable NSDictionary *)actionInformation additionalData:(nullable NSDictionary *)additionalData; -// Logging top notification. -- (void)trackTopNotificationShown:(nonnull UIView *)topNotification topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject additionalData:(nullable NSDictionary *)additionalData; - @end NS_ASSUME_NONNULL_END diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.m b/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.m index ed073722..45a13acf 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUILoggingHandler.m @@ -20,7 +20,4 @@ return nil; } -- (void)trackTopNotificationShown:(UIView *)topNotification topAlertObject:(MVMCoreTopAlertObject *)topAlertObject additionalData:(NSDictionary *)additionalData { -} - @end diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.h b/MVMCoreUI/OtherHandlers/MVMCoreUISession.h index a81c8ee2..134ae00d 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.h +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.h @@ -8,7 +8,6 @@ @import UIKit.UIView; @import MVMCore.MVMCoreSessionObject; -@class MVMCoreUITopAlertView; @class MVMCoreUISplitViewController; @class MFViewController; @class MFLoadingViewController; @@ -18,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @interface MVMCoreUISession : MVMCoreSessionObject -@property (weak, nonatomic, nullable) MVMCoreUITopAlertView *topAlertView; +@property (weak, nonatomic, nullable) UIView *topAlertView; @property (weak, nonatomic, nullable) MVMCoreUISplitViewController *splitViewController; @property (weak, nonatomic, nullable) NavigationController *navigationController; @property (weak, nonatomic, nullable) MFLoadingViewController *loadingViewController; @@ -32,9 +31,6 @@ NS_ASSUME_NONNULL_BEGIN /// indicates if the app launched successfully @property (assign, nonatomic) BOOL launchAppLoadedSuccessfully; -/// Allows a global overload of the title view of navigation item. -- (nullable UIView *)titleViewForController:(nonnull MFViewController *)controller; - /// Sets up the session as delegate for standard load view controller. Pass the view controller that will be used to present and will be disabled when load view is presented. - (void)setupAsStandardLoadViewDelegate:(nonnull UIViewController *)mainViewController; diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m index 957b5c19..42a3e769 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUISession.m +++ b/MVMCoreUI/OtherHandlers/MVMCoreUISession.m @@ -19,10 +19,6 @@ @implementation MVMCoreUISession -- (nullable UIView *)titleViewForController:(nonnull MFViewController *)controller { - return nil; -} - - (void)setupAsStandardLoadViewDelegate:(nonnull UIViewController *)mainViewController { self.mainViewController = mainViewController; [MVMCoreObject sharedInstance].loadingProtocol = self; diff --git a/MVMCoreUI/OtherHandlers/MVMCoreUIViewControllerMappingObject+Extension.swift b/MVMCoreUI/OtherHandlers/MVMCoreUIViewControllerMappingObject+Extension.swift index f126799c..34525751 100644 --- a/MVMCoreUI/OtherHandlers/MVMCoreUIViewControllerMappingObject+Extension.swift +++ b/MVMCoreUI/OtherHandlers/MVMCoreUIViewControllerMappingObject+Extension.swift @@ -19,6 +19,7 @@ public extension MVMCoreUIViewControllerMappingObject { add(toTemplateViewControllerMapping: ["modalStack": MVMCoreViewControllerProgrammaticMappingObject(with: ModalMoleculeStackTemplate.self)!]) register(template: MoleculeListTemplate.self) + register(template: CollectionTemplate.self) add(toTemplateViewControllerMapping: ["modalList": MVMCoreViewControllerProgrammaticMappingObject(with: ModalMoleculeListTemplate.self)!]) add(toTemplateViewControllerMapping: [SectionListTemplateModel.identifier: MVMCoreViewControllerProgrammaticMappingObject(with: SectionListTemplate.self)!]) add(toTemplateViewControllerMapping: [ModalSectionListTemplateModel.identifier: MVMCoreViewControllerProgrammaticMappingObject(with: ModalSectionListTemplate.self)!]) diff --git a/MVMCoreUI/SupportingFiles/Media.xcassets/Contents.json b/MVMCoreUI/SupportingFiles/Media.xcassets/Contents.json index da4a164c..73c00596 100644 --- a/MVMCoreUI/SupportingFiles/Media.xcassets/Contents.json +++ b/MVMCoreUI/SupportingFiles/Media.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/MVMCoreUI/TopAlert/MVMCoreGlobalTopAlertDelegateProtocol.h b/MVMCoreUI/TopAlert/MVMCoreGlobalTopAlertDelegateProtocol.h deleted file mode 100644 index ff35c5e9..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreGlobalTopAlertDelegateProtocol.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// MVMCoreGlobalTopAlertDelegateProtocol.h -// MVMCore -// -// Created by Pfeil, Scott Robert on 1/2/18. -// Copyright © 2018 myverizon. All rights reserved. -// - -#import -#import -@class MVMCoreTopAlertObject; - -@protocol MVMCoreGlobalTopAlertDelegateProtocol - -- (NSOperationQueuePriority)priorityForTopAlertByObject:(nonnull MVMCoreTopAlertObject *)object; - -@optional - -- (nonnull UIView *)getTopAlertView; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertAnimationDelegateProtocol.h b/MVMCoreUI/TopAlert/MVMCoreTopAlertAnimationDelegateProtocol.h deleted file mode 100644 index 52922df4..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertAnimationDelegateProtocol.h +++ /dev/null @@ -1,25 +0,0 @@ -// -// MVMCoreTopAlertAnimationDelegateProtocol.h -// mobilefirst -// -// Created by Scott Pfeil on 6/4/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import - -@protocol MVMCoreTopAlertAnimationDelegateProtocol - -// Called when the top alert is starting an animation -- (void)topAlertViewBeginAnimation; - -// Called when the top alert is ending an animation -- (void)topAlertViewFinishAnimation; - -// Called when the top alert is starting to dismiss. -- (void)topAlertWillDismiss; - -// Called when the top alert is dismissed. -- (void)topAlertDismissed; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertDelegateProtocol.h b/MVMCoreUI/TopAlert/MVMCoreTopAlertDelegateProtocol.h deleted file mode 100644 index af094838..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertDelegateProtocol.h +++ /dev/null @@ -1,23 +0,0 @@ -// -// MVMCoreTopAlertDelegateProtocol.h -// mobilefirst -// -// Created by Scott Pfeil on 6/4/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import - -@class MVMCoreTopAlertObject; - -@protocol MVMCoreTopAlertDelegateProtocol - -@optional - -- (void)topAlertViewShown:(nonnull id)topAlert topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject; -- (void)topAlertViewDismissed:(nonnull id)topAlert; - -// Called when the top alert is pressed. Determines if we should load the option the default way or not. -- (BOOL)shouldLoadTopAlertAction:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.h b/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.h deleted file mode 100644 index c222747a..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.h +++ /dev/null @@ -1,64 +0,0 @@ -// -// MVMCoreTopAlertObject.h -// mobilefirst -// -// Created by Scott Pfeil on 5/24/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import -#import - -extern NSUInteger const TopAlertDismissTime; - -@interface MVMCoreTopAlertObject : NSObject - -@property (nonatomic) BOOL persistent; -@property (nullable, nonatomic) NSString *type; -@property (nonatomic) NSOperationQueuePriority queuePriority; - -// The text -@property (nullable, strong, nonatomic) NSString *title; -@property (nullable, strong, nonatomic) NSString *message; -@property (nullable, strong, nonatomic) NSString *topMessage; - -// For the button. -@property (nullable, strong, nonatomic) NSDictionary *buttonMap; -@property (nullable, strong, nonatomic) NSDictionary *additionalData; -@property (nonatomic) BOOL useCloseButton; - -// For non action map driven button. -@property (nullable, strong, nonatomic) NSString *buttonTitle; -@property (nullable, copy, nonatomic) void (^userActionHandler)(id _Nonnull sender); - -@property (nullable, weak, nonatomic) NSObject *delegate; - -// This is used to ensure legacy style stays intact with new changes -@property (nonatomic) BOOL useNewStyle; - -// The page type used for the top alert -@property (nullable, strong, nonatomic) NSString *pageType; - -// image name or url used imageviews -@property (nullable, strong, nonatomic) NSString *imageNameOrURL; -@property (nullable, strong, nonatomic) NSString *aboveTextImageString; - -// If 0, uses default 5 seconds. -@property (nonatomic) NSInteger topAlertDismissTime; - -// Server can set color. -@property (nullable, strong, nonatomic) UIColor *backgroundColor; -@property (nullable, strong, nonatomic) UIColor *textColor; - -// The full top alert json. Currently only used for molecular. -@property (nullable, strong, nonatomic) NSDictionary *json; - -- (nullable instancetype)initWithResponseInfo:(nullable NSDictionary *)responseInfo; - -- (nullable instancetype)initWithType:(nullable NSString *)type message:(nullable NSString *)message; - -- (nullable instancetype)initWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; - -- (nullable instancetype)initWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.m b/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.m deleted file mode 100644 index 9286737a..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.m +++ /dev/null @@ -1,94 +0,0 @@ -// -// MVMCoreTopAlertObject.m -// mobilefirst -// -// Created by Scott Pfeil on 5/24/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreTopAlertObject.h" -@import MVMCore.NSDictionary_MFConvenience; -@import MVMCore.MVMCoreGetterUtility; -@import MVMCore.MVMCoreJSONConstants; -#import "MVMCoreAlertHandler.h" - -NSUInteger const TopAlertDismissTime = 5; - -@implementation MVMCoreTopAlertObject - -- (nullable instancetype)initWithResponseInfo:(nullable NSDictionary *)responseInfo { - if (self = [super init]) { - - self.type = [responseInfo string:KeyType]; - self.persistent = [[responseInfo stringForKey:KeyMessageStyle] isEqualToString:ValueMessageStyleTopPersistent]; - - self.title = [responseInfo string:KeyMessage]; - self.message = [responseInfo string:KeyUserMessage]; - self.buttonMap = [responseInfo dict:KeyButtonMap]; - self.topMessage = [responseInfo string:KeyTopMessage]; - self.imageNameOrURL = [responseInfo string:@"topAlertImageUrl"]; - self.aboveTextImageString = [responseInfo string:@"topAlertImageURLAboveText"]; - - NSString *color = [responseInfo string:@"topAlertColor"]; - if (color) { - self.backgroundColor = [MVMCoreGetterUtility getColorForHexString:color]; - } - color = [responseInfo string:@"messageColor"]; - if (color) { - self.textColor = [MVMCoreGetterUtility getColorForHexString:color]; - } - - // The default is yes if not sent by server (for legacy to work as is) - NSNumber *closeButton = [responseInfo optionalNumberForKey:KeyCloseButton]; - if (closeButton != nil) { - self.useCloseButton = [closeButton boolValue]; - } else { - self.useCloseButton = YES; - } - - self.useNewStyle = [responseInfo boolForKey:@"newTopAlertStyle"]; - - // Server driven dismiss time. - NSNumber *topAlertTime = [responseInfo optionalNumberForKey:@"topAlertTime"]; - if (topAlertTime != nil) { - self.topAlertDismissTime = [topAlertTime integerValue]; - } - } - return self; -} - -- (nullable instancetype)initWithType:(nullable NSString *)type message:(nullable NSString *)message { - return [self initWithType:type message:nil subMessage:message persistent:NO actionMap:nil additionalData:nil]; -} - -- (nullable instancetype)initWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - - if (self = [super init]) { - - self.type = type; - self.persistent = persistent; - - self.title = message; - self.message = subMessage; - self.buttonMap = actionMap; - self.additionalData = additionalData; - } - return self; -} - -- (nullable instancetype)initWithType:(nullable NSString *)type message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage persistent:(BOOL)persistent buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler { - - if (self = [super init]) { - - self.type = type; - self.persistent = persistent; - - self.title = message; - self.message = subMessage; - self.buttonTitle = buttonTitle; - self.userActionHandler = userActionHandler; - } - return self; -} - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h b/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h deleted file mode 100644 index 8a732932..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// MVMCoreTopAlertOperation.h -// mobilefirst -// -// Created by Scott Pfeil on 6/4/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -@import MVMCore.MVMCoreOperation; -#import - -@class MVMCoreTopAlertObject; - -@interface MVMCoreTopAlertOperation : MVMCoreOperation - -@property (readonly, getter=isPaused) BOOL paused; - -/// A bool for if this top alert can be displayed. It is only ready if true. -@property (nonatomic, getter=isDisplayable) BOOL displayable; - -@property (nonatomic) BOOL reAddAfterCancel; - -@property (nonnull, nonatomic, strong) MVMCoreTopAlertObject *topAlertObject; - -- (nullable instancetype)initWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject; - -/// Updates the operation with a new object -- (void)updateWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject; - -/// Updates the operation isDisplayable based on the page type. -- (void)updateDisplayableByPageType:(nullable NSString *)pageType; - -// Pauses the operation. Temporarily removes any alert. -- (void)pause; - -// Unpauses the operation, resuming any alert. -- (void)unpause; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m b/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m deleted file mode 100644 index f0011719..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m +++ /dev/null @@ -1,319 +0,0 @@ -// -// MVMCoreTopAlertOperation.m -// mobilefirst -// -// Created by Scott Pfeil on 6/4/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreTopAlertOperation.h" -#import "MVMCoreTopAlertObject.h" -#import "MVMCoreAlertHandler.h" -#import -#import - -@interface MVMCoreTopAlertOperation () { - __block BOOL _paused; - __block BOOL _displayed; - __block BOOL _animating; - __block BOOL _displayable; -} - -@property (readwrite, getter=isPaused) BOOL paused; -@property (readwrite, getter=isDisplayed) BOOL displayed; -@property (readwrite, getter=isAnimating) BOOL animating; - -// For thread safety -@property (strong, nonatomic) dispatch_queue_t pausedQueue; -@property (strong, nonatomic) dispatch_queue_t displayedQueue; -@property (strong, nonatomic) dispatch_queue_t animatingQueue; -@property (strong, nonatomic) dispatch_queue_t displayableQueue; - -@property (nonatomic, strong) dispatch_source_t timerSource; - -@end - -@implementation MVMCoreTopAlertOperation - -- (instancetype)init { - - self = [super init]; - if (self) { - self.pausedQueue = dispatch_queue_create("paused", DISPATCH_QUEUE_CONCURRENT); - self.displayedQueue = dispatch_queue_create("displayed", DISPATCH_QUEUE_CONCURRENT); - self.animatingQueue = dispatch_queue_create("animating", DISPATCH_QUEUE_CONCURRENT); - self.displayableQueue = dispatch_queue_create("displayable", DISPATCH_QUEUE_CONCURRENT); - self.displayable = YES; - } - return self; -} - -- (nullable instancetype)initWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject { - - if (self = [self init]) { - self.topAlertObject = topAlertObject; - - // Sets the queue priority for various types of alerts. - self.queuePriority = [[CoreUIObject sharedInstance].globalTopAlertDelegate priorityForTopAlertByObject:topAlertObject]; - } - return self; -} - -- (void)updateWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject { - self.topAlertObject = topAlertObject; - self.queuePriority = [[CoreUIObject sharedInstance].globalTopAlertDelegate priorityForTopAlertByObject:topAlertObject]; - if (self.isExecuting && !self.isCancelled && !self.isPaused) { - [self updateDismissTimer]; - UIView *topAlertView = [[CoreUIObject sharedInstance].globalTopAlertDelegate getTopAlertView]; - if ([topAlertView respondsToSelector:@selector(updateTopAlertWith:)]) { - [topAlertView updateTopAlertWith:topAlertObject]; - } - } -} - -- (void)updateDisplayableByPageType:(nullable NSString *)pageType { - NSArray *pages = [self.topAlertObject.json array:@"pages"]; - if (pages.count == 0) { - self.displayable = YES; - return; - } - if (pageType.length == 0) { - self.displayable = NO; - return; - } - self.displayable = [pages containsObject:pageType]; -} - -- (BOOL)isPaused { - __block BOOL isPaused; - dispatch_sync(self.pausedQueue, ^{ - isPaused = self->_paused; - }); - return isPaused; -} - -- (void)setPaused:(BOOL)paused { - dispatch_barrier_async(self.pausedQueue, ^{ - self->_paused = paused; - }); -} - -- (BOOL)isDisplayed { - __block BOOL isDisplayed; - dispatch_sync(self.displayedQueue, ^{ - isDisplayed = self->_displayed; - }); - return isDisplayed; -} - -- (void)setDisplayed:(BOOL)displayed { - dispatch_barrier_async(self.displayedQueue, ^{ - self->_displayed = displayed; - }); -} - -- (BOOL)isAnimating { - __block BOOL isAnimating; - dispatch_sync(self.animatingQueue, ^{ - isAnimating = self->_animating; - }); - return isAnimating; -} - -- (void)setAnimating:(BOOL)animating { - dispatch_barrier_async(self.animatingQueue, ^{ - self->_animating = animating; - }); -} - - -- (BOOL)isDisplayable { - __block BOOL isDisplayable; - dispatch_sync(self.displayableQueue, ^{ - isDisplayable = self->_displayable; - }); - return isDisplayable; -} - -- (void)setDisplayable:(BOOL)displayable { - if (displayable != self.isDisplayable) { - BOOL isReady = [super isReady]; - if (isReady) { - [self willChangeValueForKey:@"isReady"]; - } - dispatch_barrier_async(self.displayableQueue, ^{ - self->_displayable = displayable; - }); - if (isReady) { - [self didChangeValueForKey:@"isReady"]; - } - } -} - -- (BOOL)isReady { - if (self.isCancelled) { - return [super isReady]; - } - return [super isReady] && self.isDisplayable; -} - -- (void)main { - - // Always check for cancellation before launching the task. - if ([self checkAndHandleForCancellation]) { - return; - } - - // Do nothing if paused - if (!self.isPaused) { - - // Display alert only if alerts aren't supressed. - if (![[MVMCoreAlertHandler sharedAlertHandler] mfAlertsSupressed]) { - - // Show - if (![[CoreUIObject sharedInstance].globalTopAlertDelegate respondsToSelector:@selector(getTopAlertView)]) { - - // Needs to be a top alert view.... - [self markAsFinished]; - } else { - UIView *topAlertView = [[CoreUIObject sharedInstance].globalTopAlertDelegate getTopAlertView]; - [topAlertView showWithTopAlertObject:self.topAlertObject animationDelegate:self completionHandler:^(BOOL finished) { - - self.displayed = YES; - if (self.isCancelled) { - - // Cancelled, dismiss immediately. - [self dismissAlertView:YES]; - } else if (self.isPaused) { - - // Paused, dismiss for the time being if persistent. - [self dismissAlertView:YES]; - } else { - [self updateDismissTimer]; - } - }]; - } - } else { - [self pause]; - } - } -} - -/// Updates the timer to dismiss the top alert. -- (void)updateDismissTimer { - if (self.timerSource) { - dispatch_source_cancel(self.timerSource); - } - if (self.topAlertObject.persistent) { - return; - } - - NSInteger dismissTime; - if (self.topAlertObject.topAlertDismissTime > 0) { - dismissTime = self.topAlertObject.topAlertDismissTime; - } else { - dismissTime = TopAlertDismissTime; - } - - self.timerSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); - dispatch_source_set_timer(self.timerSource, dispatch_time(DISPATCH_TIME_NOW, dismissTime * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, (1ull * NSEC_PER_SEC) / 10); - - __weak typeof(self) weakSelf = self; - dispatch_source_set_event_handler(self.timerSource, ^{ - if (weakSelf.isFinished || [weakSelf checkAndHandleForCancellation]) { - return; - } - [weakSelf dismissAlertView:NO]; - }); - dispatch_resume(self.timerSource); -} - -- (void)cancel { - [super cancel]; - - // Do nothing if animating. - if (!self.isAnimating) { - - if (self.isDisplayed) { - [self dismissAlertView:YES]; - } else if (self.isExecuting) { - [self markAsFinished]; - } - } -} - -- (void)dismissAlertView:(BOOL)forceful { - if (self.timerSource) { - dispatch_source_cancel(self.timerSource); - } - if (self.isDisplayed && !self.isAnimating) { - - // Dismisses. - [[[CoreUIObject sharedInstance].globalTopAlertDelegate getTopAlertView] hideAlertView:forceful completionHandler:NULL]; - } -} - -- (void)pause { - - if (!self.paused) { - [self willChangeValueForKey:@"isPaused"]; - self.paused = YES; - [self didChangeValueForKey:@"isPaused"]; - } - [self dismissAlertView:YES]; -} - -- (void)unpause { - - if (self.paused) { - [self willChangeValueForKey:@"isPaused"]; - self.paused = NO; - [self didChangeValueForKey:@"isPaused"]; - } - if (self.executing) { - [self start]; - } -} - -#pragma mark - Delegate functions - -- (void)topAlertViewBeginAnimation { - self.animating = YES; -} - -- (void)topAlertViewFinishAnimation { - self.animating = NO; -} - -- (void)topAlertDismissed { - self.displayed = NO; - if (!self.isCancelled && self.isPaused && self.topAlertObject.persistent) { return; } - [self markAsFinished]; -} - -- (void)topAlertWillDismiss { - if (self.timerSource) { - dispatch_source_cancel(self.timerSource); - } -} - -- (void)topAlertCloseButtonPressed { - [self dismissAlertView:YES]; -} - -- (id)copyWithZone:(nullable NSZone *)zone { - MVMCoreTopAlertOperation *copyObject = [[MVMCoreTopAlertOperation alloc] init]; - copyObject.topAlertObject = self.topAlertObject; - copyObject.paused = self.paused; - copyObject.reAddAfterCancel = self.reAddAfterCancel; - copyObject.displayable = self.isDisplayable; - copyObject.queuePriority = self.queuePriority; - for (NSOperation *dependency in self.dependencies) { - [copyObject addDependency:dependency]; - } - copyObject.name = self.name; - copyObject.qualityOfService = self.qualityOfService; - return copyObject; -} - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertViewProtocol.h b/MVMCoreUI/TopAlert/MVMCoreTopAlertViewProtocol.h deleted file mode 100644 index e799e858..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertViewProtocol.h +++ /dev/null @@ -1,31 +0,0 @@ -// -// MVMCoreTopAlertViewProtocol.h -// MVMCore -// -// Created by Pfeil, Scott Robert on 1/2/18. -// Copyright © 2018 myverizon. All rights reserved. -// - -#import -@class MVMCoreTopAlertObject; - -@protocol MVMCoreTopAlertViewProtocol - -@optional - -/// Show based on the object -- (void)showWithTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate completionHandler:(void (^ __nullable)(BOOL finished))completionHandler; - -/// Removes the notification -- (void)hideAlertView:(BOOL)forceful completionHandler:(void (^ __nullable)(BOOL finished))completionHandler; - -/// Collapses the notification if it has a short top message. Otherwise removes notification. -- (void)collapseNotification; - -/// Updates the existing top alert with the new object -- (void)updateTopAlertWith:(nullable MVMCoreTopAlertObject *)topAlertObject; - -/// Returns if the top alert is currently utilizing the status bar. -- (BOOL)overridingStatusBar; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h deleted file mode 100644 index 4fe50237..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h +++ /dev/null @@ -1,35 +0,0 @@ -// -// MVMCoreUITopAlertBaseView.h -// mobilefirst -// -// Created by Scott Pfeil on 9/6/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import -#import -@import MVMCore.MVMCoreViewProtocol; - -@class Button; - -@interface MVMCoreUITopAlertBaseView : UIView - -// Adds a top alert action. -+ (void)addActionToButton:(nonnull Button *)button actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; - -// Returns a top alert styled label. -+ (nonnull UILabel *)topAlertLabel; - -// Returns a string for the passed in messages. -+ (nullable NSAttributedString *)getStringForMessage:(nullable NSString *)message subMessage:(nullable NSString *)subMessage color:(nullable UIColor *)color; - -// Adds a close button. -- (nonnull Button *)addCloseButtonWithAnimationDelegate:(nullable id )animationDelegate; - -// Handles making various parts accessible. -- (void)handleAccessibility; - -/// Adds the top alert accessibility prefix to the view. -+ (void)amendAccesibilityLabelForView:(nonnull UIView *)view; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m deleted file mode 100644 index 862e4b5b..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m +++ /dev/null @@ -1,93 +0,0 @@ -// -// MVMCoreUITopAlertBaseView.m -// mobilefirst -// -// Created by Scott Pfeil on 9/6/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreUITopAlertBaseView.h" -#import "MVMCoreUISplitViewController.h" -@import MVMCore.MVMCoreLoadObject; -@import MVMCore.MVMCoreRequestParameters; -#import "MVMCoreUITopAlertView.h" -@import MVMCore.MVMCoreJSONConstants; -@import MVMCore.NSDictionary_MFConvenience; -#import "UIColor+MFConvenience.h" -#import "MVMCoreUICommonViewsUtility.h" -#import "MFStyler.h" -#import "MVMCoreUISession.h" -#import -#import "MVMCoreTopAlertDelegateProtocol.h" - -@implementation MVMCoreUITopAlertBaseView - -+ (void)addActionToButton:(nonnull Button *)button actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - [button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { - // Check with the current screen if we should perform the action (not if it's the same page type) - BOOL performAction = YES; - UIViewController *topViewController = [MVMCoreUISession sharedGlobal].splitViewController.getCurrentDetailViewController; - if ([topViewController respondsToSelector:@selector(shouldLoadTopAlertAction:additionalData:)]) { - performAction = [((NSObject *)topViewController) shouldLoadTopAlertAction:actionMap additionalData:additionalData]; - } - - if (performAction) { - [[MVMCoreUIActionHandler sharedActionHandler] handleActionWithDictionary:actionMap additionalData:additionalData delegateObject:[MVMCoreUIDelegateObject createWithDelegateForAll:[MVMCoreUISession sharedGlobal].topAlertView]]; - } - }]; -} - -+ (UILabel *)topAlertLabel { - - UILabel *label = [MVMCoreUICommonViewsUtility label]; - [label setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; - label.textColor = [UIColor whiteColor]; - label.font = [MFStyler fontB1]; - label.lineBreakMode = NSLineBreakByTruncatingTail; - return label; -} - -+ (nullable NSAttributedString *)getStringForMessage:(nullable NSString *)message subMessage:(nullable NSString *)subMessage color:(nullable UIColor *)color { - - UIColor *textColor = color ?: [UIColor whiteColor]; - NSMutableAttributedString *string = [[NSMutableAttributedString alloc] init]; - if (message.length > 0) { - [string appendAttributedString:[MFStyler styleGetAttributedString:message font:[MFStyler fontB1] color:textColor]]; - } - - if (subMessage.length > 0) { - - NSString *subMessageToAppend = nil; - if (string.length > 0) { - subMessageToAppend = [NSString stringWithFormat:@"\n%@",subMessage]; - } else { - subMessageToAppend = subMessage; - } - [string appendAttributedString:[MFStyler styleGetAttributedString:subMessageToAppend font:[MFStyler fontB2] color:textColor]]; - } - return string; -} - -- (nonnull Button *)addCloseButtonWithAnimationDelegate:(nullable id )animationDelegate { - Button *closeButton = [MVMCoreUICommonViewsUtility addCloseButtonTo:self action:^(Button * _Nonnull button) { - [[MVMCoreUIActionHandler sharedActionHandler] handleActionWithDictionary:@{KeyActionType: @"dismissNotification"} additionalData:nil delegateObject:nil]; - } centeredVertically:YES]; - [closeButton.heightAnchor constraintEqualToConstant:16.0].active = YES; - [closeButton.widthAnchor constraintEqualToConstant:16.0].active = YES; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:closeButton]; - return closeButton; -} - -- (void)updateView:(CGFloat)size {} - -- (void)handleAccessibility {} - -+ (void)amendAccesibilityLabelForView:(nonnull UIView *)view { - NSString *amendment = [MVMCoreUIUtility hardcodedStringWithKey:@"top_alert_notification"]; - NSString *accessibilityLabel = view.accessibilityLabel; - if (accessibilityLabel && ![accessibilityLabel hasPrefix:amendment]) { - view.accessibilityLabel = [NSString stringWithFormat:@"%@ - %@", amendment, accessibilityLabel]; - } -} - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.h deleted file mode 100644 index 7a5519b3..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// MVMCoreUITopAlertExpandableView.h -// mobilefirst -// -// Created by Scott Pfeil on 8/18/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import -#import -#import - -@class PrimaryButton; -@class MVMCoreUITopAlertShortView; -@class MVMCoreUITopAlertMainView; -@class MVMCoreTopAlertObject; - -@interface MVMCoreUITopAlertExpandableView : MVMCoreUITopAlertBaseView - -@property (nullable, weak, nonatomic) MVMCoreUITopAlertShortView *shortView; -@property (nullable, weak, nonatomic) MVMCoreUITopAlertMainView *buttonView; - -// Setting this will hide the short view when the notification is collapsed. -@property (nonatomic) BOOL onlyShowTopMessageWhenCollapsed; - -// A flag for if the top alert expandable view should collapse automatically after being expanded -@property (nonatomic) BOOL collapseAutomaticallyAfterExpanded; - -@property (nonatomic) NSInteger collapseTime; - -// Standard -- (nullable instancetype)initWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout; - -// Used primarily for when button presses will expand or collapse. (Short view button will need to be set manually) -- (nullable instancetype)initWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout; -- (nullable instancetype)initWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout; - -// Used when button uses standard action map. -- (nullable instancetype)initWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout; - -// Convenience change functions -- (void)defaultSetup; -- (void)setTopMessage:(nullable NSString *)topMessage; -- (void)setTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; -- (void)setTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler; - -// Setters for making buttons expand and collapse the cell. -- (void)setButtonPressToExpand; -- (void)setButtonPressToCollapse; -- (void)setShortViewPressToCollapse; -- (void)setShortViewPressToExpand; - -// Animates -- (void)expand:(BOOL)animated; -- (void)collapse; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m deleted file mode 100644 index 0a548aea..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m +++ /dev/null @@ -1,432 +0,0 @@ -// -// MVMCoreUITopAlertExpandableView.m -// mobilefirst -// -// Created by Scott Pfeil on 8/18/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreUITopAlertExpandableView.h" -#import "MVMCoreUITopAlertShortView.h" -#import "MVMCoreUITopAlertMainView.h" -@import MVMCore.MVMCoreDispatchUtility; -#import -#import -@import MVMCore.MVMCoreBlockOperation; -@import MVMCore.MVMCoreNavigationHandler; -#import "MFStyler.h" -#import "NSLayoutConstraint+MFConvenience.h" -#import "MVMCoreUIUtility.h" -#import "MVMCoreUITopAlertView.h" -#import - -@interface MVMCoreUITopAlertExpandableView () - -@property (nonatomic) BOOL expanded; - -@property (nullable, weak, nonatomic) id animationDelegate; - -@property (nullable, weak, nonatomic) UIView *viewToLayout; - -@property (nullable, strong, nonatomic) NSLayoutConstraint *topLabelConstraintBottom; -@property (nullable, strong, nonatomic) NSLayoutConstraint *topConstraint; -@property (nullable, strong, nonatomic) NSLayoutConstraint *shortViewHeight; - -// Sets up the short view. -- (void)setupTopMessage:(nullable NSString *)topMessage; - -// Sets up the button view. -- (void)setupTopAlertWithButton:(MVMCoreUITopAlertMainView *)topAlertWithButton; - -// Sets up the whole view without setting button action. -- (void)setupViewWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle; - -// Sets up the whole view while setting button action. -- (void)setupViewWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; - -@end - -@implementation MVMCoreUITopAlertExpandableView - -- (void)handleAccessibility { - - if (self.shortView.label.text.length > 0 && !self.expanded) { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.shortView.label); - } else if (self.buttonView.label.text.length > 0) { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.buttonView.label); - } else { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); - } -} - -#pragma mark - Setup View - -- (void)updateView:(CGFloat)size { - [super updateView:size]; - [self.shortView updateView:size]; - [self.buttonView updateView:size]; -} - -- (nullable instancetype)init { - if (self = [super init]) { - self.translatesAutoresizingMaskIntoConstraints = NO; - self.clipsToBounds = YES; - self.expanded = NO; - } - return self; -} - -- (nullable instancetype)initWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout { - if (self = [self init]) { - self.animationDelegate = animationDelegate; - self.viewToLayout = viewTolayout; - - if (topAlertObject.useNewStyle) { - - // New style, collapses automatically after x time if there is a top message. - self.collapseTime = topAlertObject.topAlertDismissTime; - if (topAlertObject.persistent && topAlertObject.topMessage) { - _collapseAutomaticallyAfterExpanded = YES; - } - - // Setup the views. - [self setupTopMessage:topAlertObject.topMessage]; - MVMCoreUITopAlertMainView *topAlertWithButton = [[MVMCoreUITopAlertMainView alloc] initWithTopAlertObject:topAlertObject animationDelegate:animationDelegate]; - [self setupTopAlertWithButton:topAlertWithButton]; - - // Sets the color - self.shortView.label.textColor = topAlertObject.textColor ?: [[MVMCoreUITopAlertView sharedGlobal] getContentColorForType:topAlertObject.type]; - self.backgroundColor = topAlertObject.backgroundColor ?: [[MVMCoreUITopAlertView sharedGlobal] getBackgroundColorForType:topAlertObject.type]; - - if (topAlertWithButton.label.text.length > 0) { - [self expand:NO]; - } - } else { - - // Old style, has no top alert and main view is limited. - self.backgroundColor = [[MVMCoreUITopAlertView sharedGlobal] getBackgroundColorForType:topAlertObject.type]; - UIColor *contentColor = [[MVMCoreUITopAlertView sharedGlobal] getContentColorForType:topAlertObject.type]; - [self setupTopMessage:nil]; - self.shortView.label.textColor = contentColor; - MVMCoreUITopAlertMainView *topAlertWithButton = [[MVMCoreUITopAlertMainView alloc] initWithColor:self.backgroundColor contentColor:contentColor message:topAlertObject.message subMessage:nil closeButton:YES animationDelegate:animationDelegate]; - [self setupTopAlertWithButton:topAlertWithButton]; - [self expand:NO]; - } - } - return self; -} - -- (nullable instancetype)initWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout { - if (self = [self init]) { - self.animationDelegate = animationDelegate; - self.viewToLayout = viewTolayout; - [self setupViewWithTopMessage:topMessage message:message subMessage:nil contentColor:contentColor buttonTitle:buttonTitle]; - } - return self; -} - -- (nullable instancetype)initWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout { - if (self = [self init]) { - self.animationDelegate = animationDelegate; - self.viewToLayout = viewTolayout; - [self setupViewWithTopMessage:topMessage message:message subMessage:subMessage contentColor:contentColor buttonTitle:buttonTitle]; - } - return self; -} - -- (nullable instancetype)initWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData animationDelegate:(nullable id )animationDelegate viewToLayout:(nonnull UIView *)viewTolayout { - if (self = [self init]) { - self.animationDelegate = animationDelegate; - self.viewToLayout = viewTolayout; - [self setupViewWithTopMessage:topMessage message:message subMessage:subMessage contentColor:contentColor actionMap:actionMap additionalData:additionalData]; - } - return self; -} - -- (void)defaultSetup { - if (!self.shortView) { - self.translatesAutoresizingMaskIntoConstraints = NO; - self.clipsToBounds = YES; - _collapseAutomaticallyAfterExpanded = YES; - self.viewToLayout = MVMCoreUITopAlertView.sharedGlobal.superview; - self.expanded = NO; - [self setupTopMessage:nil]; - MVMCoreUITopAlertMainView *topAlertWithButton = [[MVMCoreUITopAlertMainView alloc] init]; - [topAlertWithButton defaultSetup]; - [self setupTopAlertWithButton:topAlertWithButton]; - } -} - -- (void)setupTopMessage:(nullable NSString *)topMessage { - - MVMCoreUITopAlertShortView *shortView = [[MVMCoreUITopAlertShortView alloc] initWithColor:[UIColor clearColor] message:nil actionMap:nil additionalData:nil topAlertObject:nil]; - shortView.label.font = [MFStyler fontB2]; - [self addSubview:shortView]; - self.shortView = shortView; - - [NSLayoutConstraint constraintPinSubview:shortView pinTop:YES topConstant:0 pinBottom:NO bottomConstant:0 pinLeft:YES leftConstant:0 pinRight:YES rightConstant:0]; - self.topLabelConstraintBottom = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:shortView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0]; - self.topLabelConstraintBottom.active = YES; - - self.shortViewHeight = [NSLayoutConstraint constraintWithItem:shortView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:0]; - [self setTopMessage:topMessage]; -} - -- (void)setupTopAlertWithButton:(MVMCoreUITopAlertMainView *)topAlertWithButton { - topAlertWithButton.label.alpha = 0; - topAlertWithButton.button.alpha = 0; - topAlertWithButton.backgroundColor = [UIColor clearColor]; - [self insertSubview:topAlertWithButton belowSubview:self.shortView]; - self.buttonView = topAlertWithButton; - - self.topConstraint = [NSLayoutConstraint constraintWithItem:topAlertWithButton attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.shortView attribute:NSLayoutAttributeBottom multiplier:1 constant:0]; - [NSLayoutConstraint constraintPinSubview:topAlertWithButton pinTop:NO topConstant:0 pinBottom:YES bottomConstant:0 pinLeft:YES leftConstant:0 pinRight:YES rightConstant:0]; -} - -- (void)setupViewWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle { - - [self setupTopMessage:topMessage]; - self.shortView.label.textColor = contentColor; - - MVMCoreUITopAlertMainView *topAlertWithButton = [[MVMCoreUITopAlertMainView alloc] initWithColor:[UIColor clearColor] contentColor:contentColor message:message subMessage:subMessage buttonTitle:buttonTitle userActionHandler:nil]; - [self setupTopAlertWithButton:topAlertWithButton]; -} - -- (void)setupViewWithTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - - [self setupTopMessage:topMessage]; - self.shortView.label.textColor = contentColor; - - MVMCoreUITopAlertMainView *topAlertWithButton = [[MVMCoreUITopAlertMainView alloc] initWithColor:[UIColor clearColor] contentColor:contentColor message:message subMessage:subMessage actionMap:actionMap additionalData:additionalData]; - [self setupTopAlertWithButton:topAlertWithButton]; -} - -#pragma mark - Setters - -- (void)setTopMessage:(nullable NSString *)topMessage { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - self.shortView.label.text = topMessage; - self.shortView.label.accessibilityLabel = topMessage; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.shortView.label]; - - if (topMessage && (!self.onlyShowTopMessageWhenCollapsed || !self.expanded)) { - self.shortViewHeight.active = NO; - } else if (!topMessage || (self.onlyShowTopMessageWhenCollapsed && self.expanded)) { - self.shortViewHeight.active = YES; - } - }]; -} - -- (void)setTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - [self setTopMessage:topMessage]; - [self.buttonView setupWithMessage:message subMessage:subMessage color:contentColor actionMap:actionMap additionalData:additionalData]; - }]; -} - -- (void)setTopMessage:(nullable NSString *)topMessage message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage contentColor:(nonnull UIColor *)contentColor buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - [self setTopMessage:topMessage]; - [self.buttonView setupWithMessage:message subMessage:subMessage color:contentColor buttonTitle:buttonTitle userActionHandler:userActionHandler]; - }]; -} - -- (void)setButtonPressToExpand { - __weak typeof(self) weakSelf = self; - [self.buttonView.button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { - if (weakSelf) { - [weakSelf expand:YES]; - } - }]; -} - -- (void)setButtonPressToCollapse { - __weak typeof(self) weakSelf = self; - [self.buttonView.button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { - if (weakSelf) { - [weakSelf collapse]; - } - }]; -} - -- (void)setShortViewPressToExpand { - __weak typeof(self) weakSelf = self; - self.shortView.label.accessibilityTraits = UIAccessibilityTraitButton; - [self.shortView.button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { - if (weakSelf) { - [weakSelf expand:YES]; - } - }]; -} - -- (void)setShortViewPressToCollapse { - __weak typeof(self) weakSelf = self; - self.shortView.label.accessibilityTraits = UIAccessibilityTraitButton; - [self.shortView.button addActionBlockWithEvent:UIControlEventTouchUpInside :^(Button * _Nonnull button) { - if (weakSelf) { - [weakSelf collapse]; - } - }]; -} - -- (void)setOnlyShowTopMessageWhenCollapsed:(BOOL)onlyShowTopMessageWhenCollapsed { - _onlyShowTopMessageWhenCollapsed = onlyShowTopMessageWhenCollapsed; - if (onlyShowTopMessageWhenCollapsed && self.expanded) { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - self.shortView.label.alpha = 0; - self.shortViewHeight.active = YES; - }]; - } else if (!onlyShowTopMessageWhenCollapsed && self.expanded) { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - self.shortView.label.alpha = 1; - self.shortViewHeight.active = NO; - }]; - } -} - -- (void)setCollapseAutomaticallyAfterExpanded:(BOOL)collapseAutomaticallyAfterExpanded { - _collapseAutomaticallyAfterExpanded = collapseAutomaticallyAfterExpanded; - if (collapseAutomaticallyAfterExpanded) { - [self autoCollapse]; - } -} - -#pragma mark - Expand/Collapse - -- (void)expand:(BOOL)animated { - if (!self.expanded) { - __weak typeof(self) weakSelf = self; - MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - [weakSelf performExpansion:animated onCompletion:^{ - [operation markAsFinished]; - }]; - }]; - }]; - [[MVMCoreNavigationHandler sharedNavigationHandler] addNavigationOperation:operation]; - } -} - -- (void)performExpansion:(BOOL)animated onCompletion:(void(^)(void))completionHandler { - // Must notify animation delegate before animating. - if (animated && self.animationDelegate) { - [self.animationDelegate topAlertViewBeginAnimation]; - } - - [self.viewToLayout layoutIfNeeded]; - self.topLabelConstraintBottom.active = NO; - self.topConstraint.active = YES; - self.expanded = YES; - - void(^animation)(void) = ^(void) { - self.buttonView.button.alpha = 1; - self.buttonView.label.alpha = 1; - if (self.onlyShowTopMessageWhenCollapsed) { - self.shortViewHeight.active = YES; - } - [self.viewToLayout layoutIfNeeded]; - }; - - //accessibility - added to make only top alert label and close button accessible. Posted notification when top alert is displayed - self.accessibilityElements = @[self.buttonView]; - self.shortView.isAccessibilityElement = NO; - - void(^completion)(void) = ^(void) { - UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.buttonView.label); - completionHandler(); - }; - if (animated) { - [UIView animateWithDuration:.5 animations:animation completion:^(BOOL finished) { - [self.viewToLayout layoutIfNeeded]; - - // Must notify animation delegate when animating finished. - [MVMCoreDispatchUtility performBlockInBackground:^{ - if (self.animationDelegate) { - [self.animationDelegate topAlertViewFinishAnimation]; - } - }]; - completion(); - }]; - } else { - animation(); - completion(); - } - - // Collapse after 5 seconds (if the view still exists) - [self autoCollapse]; -} - -- (void)autoCollapse { - if (self.collapseAutomaticallyAfterExpanded) { - __weak typeof(self) weakSelf = self; - NSInteger dismissTime; - if (self.collapseTime > 0) { - dismissTime = self.collapseTime; - } else { - dismissTime = TopAlertDismissTime; - } - dispatch_time_t dispatchTime = dispatch_time(DISPATCH_TIME_NOW, dismissTime * NSEC_PER_SEC); - dispatch_after(dispatchTime, dispatch_get_main_queue(), ^(void){ - typeof(self) strongSelf = weakSelf; - if (strongSelf && strongSelf.expanded && strongSelf.collapseAutomaticallyAfterExpanded) { - // If accessibility focused, delay collapse. - if ([MVMCoreUIUtility viewContainsAccessiblityFocus:strongSelf]) { - [[NSNotificationCenter defaultCenter] addObserver:strongSelf selector:@selector(accessibilityFocusChanged:) name:UIAccessibilityElementFocusedNotification object:nil]; - } else { - [strongSelf collapse]; - } - } - }); - } -} - -- (void)collapse { - if (self.expanded) { - __weak typeof(self) weakSelf = self; - MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - [weakSelf performCollapseAnimationThen:^{ - [operation markAsFinished]; - }]; - }]; - }]; - [[MVMCoreNavigationHandler sharedNavigationHandler] addNavigationOperation:operation]; - } -} - -- (void)performCollapseAnimationThen:(void(^)(void))completionHandler { - // Must notify animation delegate before animating. - if (self.animationDelegate) { - [self.animationDelegate topAlertViewBeginAnimation]; - } - [self.viewToLayout layoutIfNeeded]; - self.topConstraint.active = NO; - self.topLabelConstraintBottom.active = YES; - self.expanded = NO; - [UIView animateWithDuration:.5 animations:^{ - [self.viewToLayout layoutIfNeeded]; - self.buttonView.button.alpha = 0; - self.buttonView.label.alpha = 0; - self.shortViewHeight.active = NO; - } completion:^(BOOL finished) { - [self.viewToLayout layoutIfNeeded]; - self.accessibilityElements = @[self.shortView.label]; - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); - // Must notify animation delegate when animating finished. - [MVMCoreDispatchUtility performBlockInBackground:^{ - if (self.animationDelegate) { - [self.animationDelegate topAlertViewFinishAnimation]; - } - }]; - completionHandler(); - }]; -} - -- (void)accessibilityFocusChanged:(NSNotification *)notification { - if (notification.userInfo[UIAccessibilityFocusedElementKey] && ![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityElementFocusedNotification object:nil]; - [self collapse]; - } -} - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.h deleted file mode 100644 index 618730bc..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.h +++ /dev/null @@ -1,57 +0,0 @@ -// -// MVMCoreUITopAlertMainView.h -// mobilefirst -// -// Created by Scott Pfeil on 8/17/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import -#import -#import - -@class PillButton; -@class MVMCoreTopAlertObject; - -@interface MVMCoreUITopAlertMainView : MVMCoreUITopAlertBaseView - -@property (nullable, nonatomic, weak) UILabel *label; -@property (nullable, nonatomic, weak) PillButton *button; - -@property (nullable, nonatomic, strong) NSLayoutConstraint *height; - -// Standard -- (nullable instancetype)initWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nullable id )animationDelegate; -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nullable UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate; - -// inits with images -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nullable UIColor *)contentColor imageURL:(nullable NSString *)imageURL message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate; -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nullable UIColor *)contentColor imageURL:(nullable NSString *)imageURL message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate; - -// Sets up without image -- (void)defaultSetup; - -// Setters for label and button. -- (void)setupWithMessage:(nullable NSString *)message subMessage:(nullable NSString *)subMessage color:(nullable UIColor *)color actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; -- (void)setupWithMessage:(nullable NSString *)message subMessage:(nullable NSString *)subMessage color:(nullable UIColor *)color buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler; - -// Setters for button. -- (void)setupButtonWithActionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; -- (void)setupButtonWithButtonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler; - -// Setters for close button. -- (void)setupCloseButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate; - -#pragma mark - legacy inits - -// Legacy init: inits with a label and button, no close button or icon. -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nonnull UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData; - -// Legacy init: inits with a label and possible icon and close button. No main button. -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nonnull UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate; - -// Legacy init: inits with a label and button, no close button or icon. If passing in a block to use for the button, the top alert delegate button functions will not be called. -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nonnull UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler; - - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m deleted file mode 100644 index 1a9131a6..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m +++ /dev/null @@ -1,344 +0,0 @@ -// -// MVMCoreUITopAlertMainView.m -// mobilefirst -// -// Created by Scott Pfeil on 8/17/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreUITopAlertMainView.h" -#import "MFStyler.h" -#import "NSLayoutConstraint+MFConvenience.h" -@import MVMCore.NSDictionary_MFConvenience; -@import MVMCore.MVMCoreDispatchUtility; -#import -#import "UIColor+MFConvenience.h" -#import -#import -@import MVMCore.MVMCoreJSONConstants; -#import "MVMCoreUICommonViewsUtility.h" -#import "MVMCoreUITopAlertView.h" -#import - -@interface MVMCoreUITopAlertMainView () - -@property (nullable, strong, nonatomic) NSLayoutConstraint *labelRightConstraint; - -@property (nullable, weak, nonatomic) UIView *centerView; -@property (nullable, weak, nonatomic) LoadImageView *iconView; -@property (nullable, weak, nonatomic) LoadImageView *topIconView; -@property (nullable, weak, nonatomic) Button *closeButton; -@property (nullable, strong, nonatomic) NSString *message; -@property (nullable, strong, nonatomic) NSString *subMessage; -@property (nullable, strong, nonatomic) UIColor *contentColor; - -@end - -@implementation MVMCoreUITopAlertMainView - -- (void)handleAccessibility { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.label); -} - -#pragma mark - Setup View - -- (void)updateView:(CGFloat)size { - [super updateView:size]; - self.label.attributedText = [MVMCoreUITopAlertBaseView getStringForMessage:self.message subMessage:self.subMessage color:self.contentColor]; - self.label.accessibilityLabel = self.label.text; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.label]; - [self.button updateView:size]; -} - -- (nullable instancetype)init { - if (self = [super init]) { - self.translatesAutoresizingMaskIntoConstraints = NO; - self.clipsToBounds = YES; - self.height = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:96]; - self.height.active = YES; - } - return self; -} - -- (nullable instancetype)initWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nullable id )animationDelegate { - if (self = [self init]) { - UIColor *contentColor = topAlertObject.textColor ?: [[MVMCoreUITopAlertView sharedGlobal] getContentColorForType:topAlertObject.type]; - self.backgroundColor = topAlertObject.backgroundColor ?: [[MVMCoreUITopAlertView sharedGlobal] getBackgroundColorForType:topAlertObject.type]; - [self setupViewWithLabelAndImage:topAlertObject.imageNameOrURL topImage:topAlertObject.aboveTextImageString]; - [self setupCloseButton:topAlertObject.useCloseButton animationDelegate:animationDelegate]; - [self setupWithMessage:topAlertObject.title subMessage:topAlertObject.message color:contentColor actionMap:topAlertObject.buttonMap additionalData:topAlertObject.additionalData]; - } - return self; -} - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nullable UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate { - - // Handles all scenarios. - if (self = [self init]) { - self.backgroundColor = color; - [self setupViewWithLabelAndImage:nil topImage:nil]; - [self setupCloseButton:closeButton animationDelegate:animationDelegate]; - [self setupWithMessage:message subMessage:subMessage color:contentColor actionMap:actionMap additionalData:additionalData]; - } - return self; -} - -#pragma mark - inits with images - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nullable UIColor *)contentColor imageURL:(nullable NSString *)imageURL message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate { - - // Handles all scenarios. - if (self = [self init]) { - self.backgroundColor = color; - [self setupViewWithLabelAndImage:imageURL topImage:nil]; - [self setupCloseButton:closeButton animationDelegate:animationDelegate]; - [self setupWithMessage:message subMessage:subMessage color:contentColor actionMap:actionMap additionalData:additionalData]; - } - return self; -} - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nullable UIColor *)contentColor imageURL:(nullable NSString *)imageURL message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate { - - // No main button. - if (self = [self init]) { - self.backgroundColor = color; - [self setupViewWithLabelAndImage:imageURL topImage:nil]; - [self setupCloseButton:closeButton animationDelegate:animationDelegate]; - [self setupWithMessage:message subMessage:subMessage color:contentColor buttonTitle:nil userActionHandler:NULL]; - } - return self; -} - -#pragma mark - setup - -- (void)defaultSetup { - if (!self.label) { - self.translatesAutoresizingMaskIntoConstraints = NO; - self.clipsToBounds = YES; - self.height = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:96]; - self.height.active = YES; - [self setupViewWithLabelAndImage:nil topImage:nil]; - } -} - -- (void)setupViewWithLabelAndImage:(NSString *)imageURL topImage:(NSString *)topImageString { - - UIView *centerView = [MVMCoreUICommonViewsUtility commonView]; - [self addSubview:centerView]; - self.centerView = centerView; - [centerView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=0-[centerView]->=0-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(centerView)]]; - - // Add the label. - UILabel *label = self.label; - if (label) { - [label removeFromSuperview]; - } else { - label = [MVMCoreUITopAlertBaseView topAlertLabel]; - self.label = label; - } - [centerView addSubview:label]; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[label]-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:nil views:NSDictionaryOfVariableBindings(label)]]; - [label setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; - - // Add Top Image - if (self.topIconView) { - [self.topIconView removeFromSuperview]; - } - if (topImageString) { - LoadImageView *imageView = [[LoadImageView alloc] initWithPinnedEdges:UIRectEdgeLeft | UIRectEdgeTop | UIRectEdgeBottom]; - imageView.imageView.contentMode = UIViewContentModeScaleAspectFit; - imageView.addSizeConstraintsForAspectRatio = YES; - imageView.translatesAutoresizingMaskIntoConstraints = NO; - [centerView addSubview:imageView]; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-out-[imageView]-between-[label]-out-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:@{@"out":@(PaddingTwo),@"between":@(PaddingOne)} views:NSDictionaryOfVariableBindings(imageView,label)]]; - [imageView.leftAnchor constraintEqualToAnchor:label.leftAnchor].active = YES; - [imageView.rightAnchor constraintEqualToAnchor:label.rightAnchor].active = YES; - [imageView loadImageWithName:topImageString height:@(12)]; - } else { - [NSLayoutConstraint constraintPinSubview:label pinTop:YES topConstant:PaddingTwo pinBottom:YES bottomConstant:PaddingTwo pinLeft:NO leftConstant:0 pinRight:NO rightConstant:0]; - } - - // Add main image. - if (self.iconView) { - [self.iconView removeFromSuperview]; - } - - CGFloat horizontalPadding = [MFStyler defaultHorizontalPaddingForApplicationWidth]; - if (imageURL) { - LoadImageView *imageView = [[LoadImageView alloc] init]; - imageView.translatesAutoresizingMaskIntoConstraints = NO; - [imageView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; - [self addSubview:imageView]; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|->=space-[imageView]->=space-|" options:NSLayoutFormatDirectionLeadingToTrailing metrics:@{@"space":@(PaddingFive)} views:NSDictionaryOfVariableBindings(imageView)]]; - [NSLayoutConstraint activateConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-space-[imageView]-space-[centerView]" options:NSLayoutFormatDirectionLeadingToTrailing metrics:@{@"space":@(horizontalPadding)} views:NSDictionaryOfVariableBindings(imageView,centerView)]]; - [NSLayoutConstraint constraintWithItem:imageView attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0].active = YES; - self.iconView = imageView; - [imageView loadImageWithName:imageURL width:@(32)]; - } else { - [NSLayoutConstraint constraintWithItem:centerView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1 constant:horizontalPadding].active = YES; - } -} - -- (void)setupCloseButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate { - - if (closeButton && !self.closeButton) { - self.closeButton = [self addCloseButtonWithAnimationDelegate:animationDelegate]; - [self.closeButton setTintColor:self.contentColor ?: [UIColor whiteColor]]; - } else if (!closeButton && self.closeButton) { - [self.closeButton removeFromSuperview]; - self.closeButton = nil; - } -} - -- (void)setupWithButton:(BOOL)showButton { - - // Setup the button. - CGFloat horizontalPadding = [MFStyler defaultHorizontalPaddingForApplicationWidth]; - if (showButton) { - - if (!self.button) { - - // remove label right constraint - self.labelRightConstraint.active = NO; - - // Sets up to use a button action. Always uses the top view controller - PillButton *button = [[PillButton alloc] initAsPrimaryButton:false makeTiny:true]; - [button setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; - [button setContentHuggingPriority:800 forAxis:UILayoutConstraintAxisHorizontal]; - - button.translatesAutoresizingMaskIntoConstraints = NO; - [self addSubview:button]; - [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeCenterY relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeCenterY multiplier:1.0 constant:0].active = YES; - [NSLayoutConstraint constraintWithItem:button attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.centerView attribute:NSLayoutAttributeRight multiplier:1 constant:PaddingThree].active = YES; - [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:button attribute:NSLayoutAttributeRight multiplier:1 constant:(self.closeButton ? PaddingTen : horizontalPadding)].active = YES; - self.button = button; - } - } else { - - // remove button. - if (self.button) { - [self.button removeFromSuperview]; - self.button = nil; - } - - if (!self.labelRightConstraint) { - self.labelRightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.centerView attribute:NSLayoutAttributeRight multiplier:1 constant:(self.closeButton ? PaddingTen : horizontalPadding)]; - } - self.labelRightConstraint.active = YES; - } -} - - -#pragma mark - Setters - -- (void)setupWithMessage:(nullable NSString *)message subMessage:(nullable NSString *)subMessage color:(nullable UIColor *)color actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - self.message = message; - self.subMessage = subMessage; - self.contentColor = color; - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - - // Sets the string - self.label.attributedText = [MVMCoreUITopAlertBaseView getStringForMessage:message subMessage:subMessage color:color]; - self.label.accessibilityLabel = self.label.text; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.label]; - - // Sets the button - [self setupButtonWithActionMap:actionMap additionalData:additionalData]; - if (color) { - self.button.layer.borderColor = color.CGColor; - [self.button setTitleColor:color forState:UIControlStateNormal]; - [self.closeButton setTintColor:color]; - } - }]; -} - -- (void)setupWithMessage:(nullable NSString *)message subMessage:(nullable NSString *)subMessage color:(nullable UIColor *)color buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler { - self.message = message; - self.subMessage = subMessage; - self.contentColor = color; - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - - // Sets the string - self.label.attributedText = [MVMCoreUITopAlertBaseView getStringForMessage:message subMessage:subMessage color:color]; - self.label.accessibilityLabel = self.label.text; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.label]; - - // Sets the color - if (color) { - self.button.layer.borderColor = color.CGColor; - [self.button setTitleColor:color forState:UIControlStateNormal]; - [self.closeButton setTintColor:color]; - } - - // Sets the button - [self setupButtonWithButtonTitle:buttonTitle userActionHandler:userActionHandler]; - }]; -} - -- (void)setupButtonWithActionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - BOOL showButton = actionMap != nil; - [self setupWithButton:showButton]; - if (showButton) { - [self.button setTitle:[actionMap stringForKey:KeyTitle] forState:UIControlStateNormal]; - self.button.accessibilityLabel = [self.button titleForState:UIControlStateNormal]; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.button]; - - [MVMCoreUITopAlertBaseView addActionToButton:self.button actionMap:actionMap additionalData:additionalData]; - } - }]; -} - -- (void)setupButtonWithButtonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler { - - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - BOOL showButton = buttonTitle.length > 0; - [self setupWithButton:showButton]; - [self.button setTitle:buttonTitle forState:UIControlStateNormal]; - self.button.accessibilityLabel = [self.button titleForState:UIControlStateNormal]; - [MVMCoreUITopAlertBaseView amendAccesibilityLabelForView:self.button]; - if (showButton && userActionHandler) { - [self.button addActionBlockWithEvent:UIControlEventTouchUpInside :userActionHandler]; - } - }]; -} - -#pragma mark - legacy inits - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nonnull UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData { - - // No icon or close button. - if (self = [self init]) { - self.backgroundColor = color; - [self setupViewWithLabelAndImage:nil topImage:nil]; - [self setupWithMessage:message subMessage:subMessage color:contentColor actionMap:actionMap additionalData:additionalData]; - } - return self; -} - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nonnull UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage closeButton:(BOOL)closeButton animationDelegate:(nullable id )animationDelegate { - - // No main button. - if (self = [self init]) { - self.backgroundColor = color; - [self setupViewWithLabelAndImage:nil topImage:nil]; - [self setupWithMessage:message subMessage:subMessage color:contentColor buttonTitle:nil userActionHandler:NULL]; - [self setupCloseButton:closeButton animationDelegate:animationDelegate]; - } - return self; -} - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color contentColor:(nonnull UIColor *)contentColor message:(nullable NSString *)message subMessage:(nullable NSString *)subMessage buttonTitle:(nullable NSString *)buttonTitle userActionHandler:(nullable void (^)(id _Nonnull sender))userActionHandler { - - // No icon or close button. Custom button action. - if (self = [self init]) { - self.backgroundColor = color; - [self setupViewWithLabelAndImage:nil topImage:nil]; - [self setupWithMessage:message subMessage:subMessage color:contentColor buttonTitle:buttonTitle userActionHandler:userActionHandler]; - } - return self; -} - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.h deleted file mode 100644 index 6241a71e..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// MVMCoreUITopAlertShortView.h -// mobilefirst -// -// Created by Scott Pfeil on 7/20/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import -#import - -@class MVMCoreTopAlertObject; -@class Button; - -@interface MVMCoreUITopAlertShortView : MVMCoreUITopAlertBaseView - -@property (nullable, weak, nonatomic) UILabel *label; -@property (nullable, weak, nonatomic) Button *button; - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color message:(nullable NSString *)message actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData topAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.m deleted file mode 100644 index 3890cdd3..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.m +++ /dev/null @@ -1,67 +0,0 @@ -// -// MVMCoreUITopAlertShortView.m -// mobilefirst -// -// Created by Scott Pfeil on 7/20/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreUITopAlertShortView.h" -#import "MFStyler.h" -#import "NSLayoutConstraint+MFConvenience.h" -#import "MVMCoreUIConstants.h" -#import - -@interface MVMCoreUITopAlertShortView () - -@end - -@implementation MVMCoreUITopAlertShortView - -- (void)updateView:(CGFloat)size { - [super updateView:size]; - self.label.font = [MFStyler fontB1]; -} - -- (nullable instancetype)initWithColor:(nonnull UIColor *)color message:(nullable NSString *)message actionMap:(nullable NSDictionary *)actionMap additionalData:(nullable NSDictionary *)additionalData topAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject { - - if (self = [super init]) { - - self.backgroundColor = color; - self.translatesAutoresizingMaskIntoConstraints = NO; - - UILabel *label = [MVMCoreUITopAlertBaseView topAlertLabel]; - label.numberOfLines = 1; - label.textAlignment = NSTextAlignmentCenter; - label.text = message; - [self addSubview:label]; - [label setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical]; - [NSLayoutConstraint constraintPinSubview:label pinTop:YES topConstant:0 pinBottom:YES bottomConstant:4 pinLeft:YES leftConstant:PaddingThree pinRight:YES rightConstant:PaddingThree]; - self.label = label; - - // Sets up to use a button action. - Button *button = [Button buttonWithType:UIButtonTypeCustom]; - button.backgroundColor = [UIColor clearColor]; - button.translatesAutoresizingMaskIntoConstraints = NO; - [button setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisVertical]; - [button setContentCompressionResistancePriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; - [self addSubview:button]; - [NSLayoutConstraint constraintPinSubview:button pinTop:YES topConstant:0 pinBottom:YES bottomConstant:-5 pinLeft:YES leftConstant:0 pinRight:YES rightConstant:0]; - if (actionMap) { - [MVMCoreUITopAlertBaseView addActionToButton:button actionMap:actionMap additionalData:additionalData]; - } - self.button = button; - - // Listen for status bar touches. - [[NSNotificationCenter defaultCenter] addObserverForName:NotificationStatusBarTouched object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { - [button sendActionsForControlEvents:UIControlEventAllEvents]; - }]; - } - return self; -} - -- (void)handleAccessibility { - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.label); -} - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift deleted file mode 100644 index 0ae1e879..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift +++ /dev/null @@ -1,143 +0,0 @@ -// -// MVMCoreUITopAlertView+Extension.swift -// MVMCoreUI -// -// Created by Scott Pfeil on 9/11/20. -// Copyright © 2020 Verizon Wireless. All rights reserved. -// - -import Foundation -import MVMCore - -/// Allows top alerts to determine the status bar color and style. -protocol StatusBarUI { - func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) -} - -public extension MVMCoreUITopAlertView { - - /// Registers with the notification center to know when json is updated. - @objc func registerWithNotificationCenter() { - NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) - } - - private func getDelegateObject() -> MVMCoreUIDelegateObject { - // TODO: Top alert view is current delegate. Should move to current view controller eventually? - return MVMCoreUIDelegateObject.create(withDelegateForAll: self) - } - - /// Checks for new top alert json - @objc private func responseJSONUpdated(notification: Notification) { - guard let loadObject = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject) else { return } - - // Dismiss any top alerts that server wants us to dismiss/ - if let disableType = loadObject.responseInfoMap?.optionalStringForKey("disableType") { - MVMCoreAlertHandler.shared()?.hideTopAlertView(ofType: disableType) - } - - // Show any new top alert. - guard let responseJSON = loadObject.responseJSON, - let json = responseJSON.optionalDictionaryForKey(KeyTopAlert), - let model = decodeTopNotification(with: json, delegateObject: getDelegateObject()) else { return } - showTopAlert(with: model) - } - - /// Decodes the json into a TopNotificationModel - private func decodeTopNotification(with json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) -> TopNotificationModel? { - do { - return try TopNotificationModel.decode(json: json, delegateObject: delegateObject) - } catch { - if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") { - MVMCoreUILoggingHandler.addError(toLog: errorObject) - } - return nil - } - } - - /// Shows the top alert with the model. - func showTopAlert(with model: TopNotificationModel) { - let object = model.createTopAlertObject() - guard !checkAndUpdateExisting(with: object), - let operation = MVMCoreTopAlertOperation(topAlertObject: object) else { return } - 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 } - for case let operation as MVMCoreTopAlertOperation in queue { - guard topAlertObject.json != nil, - operation.topAlertObject.type == topAlertObject.type else { continue } - operation.update(with: topAlertObject) - let pageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType - operation.updateDisplayable(byPageType: pageType) - MVMCoreAlertHandler.shared()?.reevaluteQueue() - return true - } - return false - } - - /// Updates the current top alert molecule with the new object - @objc func updateMolecule(with topAlertObject: MVMCoreTopAlertObject) { - guard topAlertObject.type == self.topAlertObject?.type else { return } - let delegateObject = getDelegateObject() - guard let newJson = topAlertObject.json, - let newModel = decodeTopNotification(with: newJson, delegateObject: delegateObject), - let newModelName = ModelRegistry.getMoleculeClass(newModel.molecule)?.nameForReuse(with: newModel.molecule, delegateObject), - let currentJson = self.topAlertObject?.json, - let currentModel = decodeTopNotification(with: currentJson, delegateObject: delegateObject), - let currentModelName = ModelRegistry.getMoleculeClass(currentModel.molecule)?.nameForReuse(with: currentModel.molecule, delegateObject), - newModelName == currentModelName, - let molecule = currentAlert as? MoleculeViewProtocol else { - // Log that we couldn't update. - if let errorObject = MVMCoreErrorObject(title: nil, message: nil, messageToLog: nil, code: ErrorCode.parsingJSON.rawValue, domain: ErrorDomainNative, location: "TopNotification update \(String(describing: topAlertObject.type))") { - MVMCoreUILoggingHandler.addError(toLog: errorObject) - } - return - } - MVMCoreDispatchUtility.performBlock(onMainThread: { - // Update molecule - molecule.reset() - molecule.set(with: newModel.molecule, delegateObject, nil) - (molecule as? MVMCoreViewProtocol)?.updateView(self.bounds.width) - - // Update status bar. - guard let statusBarDelegate = molecule as? StatusBarUI else { return } - let statusBarUI = statusBarDelegate.getStatusBarUI() - MVMCoreUISplitViewController.main()?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style) - }) - } - - /// Returns the top alert molecule to use and status bar color legacy style. - @objc func molecule(for topAlertObject: MVMCoreTopAlertObject, statusBarColor: AutoreleasingUnsafeMutablePointer?, statusBarStyle: UnsafeMutablePointer?) -> UIView? { - do { - let delegateObject = MVMCoreUIDelegateObject.create(withDelegateForAll: self) - guard let json = topAlertObject.json else { return nil } - let model = try TopNotificationModel.decode(json: json, delegateObject: delegateObject) - guard let molecule = ModelRegistry.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else { - throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped") - } - if let castView = molecule as? StatusBarUI { - let (color, style) = castView.getStatusBarUI() - statusBarColor?.pointee = color - statusBarStyle?.pointee = style - } - // TODO: Temporary, waiting for actual restriction from design. - molecule.heightAnchor.constraint(lessThanOrEqualToConstant: 140).isActive = true - return molecule - } catch { - if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") { - MVMCoreUILoggingHandler.addError(toLog: errorObject) - } - return nil - } - } -} - -extension MVMCoreUITopAlertView: ActionDelegateProtocol {} diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h deleted file mode 100644 index fad65957..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h +++ /dev/null @@ -1,46 +0,0 @@ -// -// MVMCoreUITopAlertView.h -// myverizon -// -// Created by Chris Yang on 2/3/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import -@import MVMCore.MVMCoreLoadDelegateProtocol; -@import MVMCore.MVMCoreActionDelegateProtocol; -@import MVMCore.MVMCorePresentationDelegateProtocol; -@import MVMCore.MVMCoreViewProtocol; -#import -#import -#import - -@class MVMCoreTopAlertObject; - -@interface MVMCoreUITopAlertView : UIView - -// Delegate for the top alert view -@property (nonatomic, nullable, weak) id animationDelegate; - -// Current top alert object -@property (strong, nullable, nonatomic) MVMCoreTopAlertObject *topAlertObject; - -/// Current top alert view. -@property (weak, nullable, nonatomic, readonly) UIView *currentAlert; - -// Returns the top alert view -+ (nullable instancetype)sharedGlobal; - -// Returns a TopAlertView with the mvm styling. Also sets the property in the session. -+ (nullable instancetype)setupTopAlertView; - -// Can be subclassed for custom views. -- (nonnull UIView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle; - -/// Get the background color based on the type -- (nonnull UIColor *)getBackgroundColorForType:(nullable NSString *)type; - -/// Get the content color based on the type -- (nonnull UIColor *)getContentColorForType:(nullable NSString *)type; - -@end diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m deleted file mode 100644 index 292d9132..00000000 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m +++ /dev/null @@ -1,292 +0,0 @@ -// -// MVMCoreUITopAlertView.m -// myverizon -// -// Created by Chris Yang on 2/3/16. -// Copyright © 2016 Verizon Wireless. All rights reserved. -// - -#import "MVMCoreUITopAlertView.h" -#import "MVMCoreUICommonViewsUtility.h" -#import "UIColor+MFConvenience.h" -#import "MVMCoreUITopAlertShortView.h" -#import "MVMCoreUITopAlertMainView.h" -#import "MVMCoreUITopAlertExpandableView.h" -#import "MVMCoreUISplitViewController.h" -#import "NSLayoutConstraint+MFConvenience.h" -#import "MVMCoreUISession.h" -#import "MVMCoreUIUtility.h" -#import -#import -@import MVMCore.MVMCoreLoadHandler; -@import MVMCore.MVMCoreNavigationHandler; -@import MVMCore.MVMCoreBlockOperation; -#import -#import -@import MVMCore.NSDictionary_MFConvenience; -@import MVMCore.MVMCoreRequestParameters; -@import MVMCore.MVMCoreJSONConstants; -@import MVMCore.MVMCoreDispatchUtility; - -NSString * const MFAccTopAlertClosed = @"Top alert notification is closed."; - -@interface MVMCoreUITopAlertView () - -@property (weak, nullable, nonatomic, readwrite) UIView *currentAlert; -@property (strong, nonatomic) NSLayoutConstraint *height; - -@property (weak, nonatomic) MVMCoreUITopAlertExpandableView *topAlertClearspotView; -@property (strong, nonatomic) NSString *time; - -/// Used if we delayed the collapse due to accessibility. -@property (copy, nonatomic) void (^ hideCompletionHandler)(BOOL finished); - -@property (nonatomic) BOOL currentAlertOverridingStatusBar; - -@end - -@implementation MVMCoreUITopAlertView - -- (instancetype)init { - self = [super init]; - if (self) { - [self setupView]; - } - return self; -} - -- (instancetype)initWithFrame:(CGRect)frame { - self = [super initWithFrame:frame]; - if (self) { - [self setupView]; - } - return self; -} - -- (instancetype)initWithCoder:(NSCoder *)coder { - self = [super initWithCoder:coder]; - if (self) { - [self setupView]; - } - return self; -} - -+ (nullable instancetype)sharedGlobal { - return [MVMCoreUISession sharedGlobal].topAlertView; -} - -+ (nullable instancetype)setupTopAlertView { - MVMCoreUITopAlertView *topAlertView = [[self alloc] init]; - topAlertView.translatesAutoresizingMaskIntoConstraints = NO; - [MVMCoreUISession sharedGlobal].topAlertView = topAlertView; - return topAlertView; -} - -- (void)setupView { - if (self.height) { return; } - self.clipsToBounds = YES; - self.height = [self.heightAnchor constraintEqualToConstant:0]; - self.height.active = YES; - [self registerWithNotificationCenter]; -} - -- (void)updateView:(CGFloat)size { - if ([self.currentAlert respondsToSelector:@selector(updateView:)]) { - [((UIView *)(self.currentAlert)) updateView:size]; - } -} - -- (nonnull UIView *)topAlertViewForTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate statusBarColor:(UIColor *_Nullable *_Nullable)statusBarColor statusBarStyle:(UIStatusBarStyle *_Nullable)statusBarStyle { - if (topAlertObject.json) { - return [self moleculeFor:topAlertObject statusBarColor:statusBarColor statusBarStyle:statusBarStyle]; - } else { - MVMCoreUITopAlertExpandableView *view = [[MVMCoreUITopAlertExpandableView alloc] initWithTopAlertObject:topAlertObject animationDelegate:animationDelegate viewToLayout:self.superview]; - if (statusBarColor && view.shortView.label.text) { - *statusBarColor = view.backgroundColor; - - if (statusBarStyle) { - CGFloat greyScale = 0; - if ([view.shortView.label.textColor getWhite:&greyScale alpha:nil]) { - *statusBarStyle = greyScale > 0.5 ? UIStatusBarStyleLightContent : UIStatusBarStyleDefault; - } - } - } - return view; - } -} - -- (nonnull UIColor *)getBackgroundColorForType:(nullable NSString *)type { - if ([type isEqualToString:ValueTypeError]) { - return [UIColor mvmOrange]; - } else { - return [UIColor mvmGreen]; - } -} - -- (nonnull UIColor *)getContentColorForType:(nullable NSString *)type { - if ([type isEqualToString:ValueTypeError]) { - return [UIColor blackColor]; - } else { - return [UIColor whiteColor]; - } -} - -- (void)updateAccessibilityForTopAlert:(nullable UIView *)view { - // Update accessibility with top alert - if ([view isKindOfClass:[MVMCoreUITopAlertBaseView class]]) { - [((MVMCoreUITopAlertBaseView *)view) handleAccessibility]; - } else { - id accessibilityArgument = view; - if ([view conformsToProtocol:@protocol(AccessibilityProtocol)]) { - accessibilityArgument = [((id )view) getAccessibilityLayoutChangedArgument]; - } - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, accessibilityArgument); - } -} - -- (void)showAlertView:(nullable UIView *)view topAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { - - __weak typeof(self) weakSelf = self; - MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - if (weakSelf.currentAlert.superview) { - [weakSelf.currentAlert removeFromSuperview]; - } - [weakSelf addSubview:view]; - [NSLayoutConstraint constraintPinSubviewToSuperview:view]; - weakSelf.currentAlert = view; - - [weakSelf.animationDelegate topAlertViewBeginAnimation]; - [weakSelf.superview layoutIfNeeded]; - [UIView animateWithDuration:.5 animations:^{ - weakSelf.height.active = NO; - [weakSelf.superview layoutIfNeeded]; - } completion:^(BOOL finished) { - [weakSelf.superview layoutIfNeeded]; - [weakSelf.animationDelegate topAlertViewFinishAnimation]; - - [weakSelf updateAccessibilityForTopAlert:view]; - - [MVMCoreDispatchUtility performBlockInBackground:^{ - if ([weakSelf.topAlertObject.delegate respondsToSelector:@selector(topAlertViewShown:topAlertObject:)]) { - [weakSelf.topAlertObject.delegate topAlertViewShown:view topAlertObject:topAlertObject]; - } - [[MVMCoreUILoggingHandler sharedLoggingHandler] trackTopNotificationShown:view topAlertObject:topAlertObject additionalData:nil]; - [operation markAsFinished]; - completionHandler(finished); - }]; - }]; - }]; - }]; - [[MVMCoreNavigationHandler sharedNavigationHandler] addNavigationOperation:operation]; -} - - -/// If the voice over user leaves top alert focus, hide. -- (void)accessibilityFocusChanged:(NSNotification *)notification { - if (notification.userInfo[UIAccessibilityFocusedElementKey] && ![MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIAccessibilityElementFocusedNotification object:nil]; - [self hideAlertView:YES completionHandler:self.hideCompletionHandler]; - self.hideCompletionHandler = nil; - } -} - -#pragma mark - MVMCoreTopAlertViewProtocol - -- (void)showWithTopAlertObject:(nullable MVMCoreTopAlertObject *)topAlertObject animationDelegate:(nonnull id )animationDelegate completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { - - self.animationDelegate = animationDelegate; - dispatch_async(dispatch_get_main_queue(), ^{ - - self.topAlertObject = topAlertObject; - self.topAlertClearspotView = nil; - - UIColor *statusBarColor = nil; - UIStatusBarStyle statusBarStyle = UIStatusBarStyleDefault; - UIView *view = [self topAlertViewForTopAlertObject:topAlertObject animationDelegate:animationDelegate statusBarColor:&statusBarColor statusBarStyle:&statusBarStyle]; - if ([view conformsToProtocol:@protocol(MVMCoreViewProtocol)]) { - [((UIView *)view) updateView:CGRectGetWidth(self.bounds)]; - } - if (statusBarColor) { - self.currentAlertOverridingStatusBar = YES; - [[MVMCoreUISplitViewController mainSplitViewController] setStatusBarBackgroundColor:statusBarColor style:statusBarStyle]; - } - [self showAlertView:view topAlertObject:topAlertObject completionHandler:completionHandler]; - }); -} - -- (void)hideAlertView:(BOOL)forceful completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { - // If accessible and focused, do not collapse until unfocused. - if (!forceful && [MVMCoreUIUtility viewContainsAccessiblityFocus:self]) { - self.hideCompletionHandler = completionHandler; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(accessibilityFocusChanged:) name:UIAccessibilityElementFocusedNotification object:nil]; - return; - } - - __weak typeof(self) weakSelf = self; - MVMCoreBlockOperation *operation = [MVMCoreBlockOperation blockOperationWithBlock:^(MVMCoreBlockOperation * _Nonnull operation) { - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - [weakSelf.animationDelegate topAlertWillDismiss]; - [weakSelf.animationDelegate topAlertViewBeginAnimation]; - - // accessibility - below line added to notify VI user through voiceover user when the top alert is closed - UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, [MVMCoreUIUtility hardcodedStringWithKey:@"AccTopAlertClosed"]); - - [UIView animateWithDuration:.5 animations:^{ - weakSelf.height.active = YES; - [weakSelf.superview layoutIfNeeded]; - } completion:^(BOOL finished) { - [weakSelf.animationDelegate topAlertViewFinishAnimation]; - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); - - UIView *view = weakSelf.currentAlert; - if (view.superview) { - [view removeFromSuperview]; - } - - [MVMCoreDispatchUtility performBlockInBackground:^{ - if ([weakSelf.topAlertObject.delegate respondsToSelector:@selector(topAlertViewDismissed:)]) { - [weakSelf.topAlertObject.delegate topAlertViewDismissed:view]; - } - [operation markAsFinished]; - if (completionHandler) { - completionHandler(finished); - } - [weakSelf.animationDelegate topAlertDismissed]; - weakSelf.topAlertObject = nil; - if (weakSelf.currentAlertOverridingStatusBar) { - weakSelf.currentAlertOverridingStatusBar = NO; - [MVMCoreDispatchUtility performBlockOnMainThread:^{ - [[MVMCoreUISplitViewController mainSplitViewController] setStatusBarForCurrentViewController]; - }]; - } - }]; - }]; - }]; - }]; - [[MVMCoreNavigationHandler sharedNavigationHandler] addNavigationOperation:operation]; -} - - -- (void)updateTopAlertWith:(MVMCoreTopAlertObject *)topAlertObject { - [self updateMoleculeWith:topAlertObject]; -} - -- (void)collapseNotification { - if (self.currentAlert) { - if ([self.currentAlert isKindOfClass:[MVMCoreUITopAlertExpandableView class]] && ((MVMCoreUITopAlertExpandableView *)self.currentAlert).shortView.label.text.length > 0) { - - // We have a short message, collapse to show short message. - [((MVMCoreUITopAlertExpandableView *)self.currentAlert) collapse]; - } else { - // Top alert is not collapsable, remove it instead. - [self hideAlertView:NO completionHandler:NULL]; - } - } -} - -- (BOOL)overridingStatusBar { - return self.currentAlertOverridingStatusBar; -} - -@end diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift index ffb74ef9..e04de6bb 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift +++ b/MVMCoreUI/Utility/MVMCoreUIUtility+Extension.swift @@ -7,7 +7,7 @@ // import UIKit - +import MVMCore public extension MVMCoreUIUtility { @@ -66,3 +66,25 @@ public extension MVMCoreUIUtility { return nil } } + +@objc +public extension MVMCoreUIUtility { + @objc @MainActor + static func getVisibleViewController() -> UIViewController? { + var viewController = NavigationHandler.shared().getViewControllerToPresentOn() + while let presentedController = viewController?.presentedViewController, + !presentedController.isBeingDismissed { + viewController = presentedController + } + if let navigationController = viewController as? UINavigationController { + viewController = navigationController.topViewController + } + if let viewController = viewController { + return getViewControllerTraversingManagers(viewController) + } else if let viewController = MVMCoreUISession.sharedGlobal()?.navigationController?.topViewController { + return getViewControllerTraversingManagers(viewController) + } else { + return nil + } + } +} diff --git a/MVMCoreUI/Utility/MVMCoreUIUtility.m b/MVMCoreUI/Utility/MVMCoreUIUtility.m index 887d7438..ad7366df 100644 --- a/MVMCoreUI/Utility/MVMCoreUIUtility.m +++ b/MVMCoreUI/Utility/MVMCoreUIUtility.m @@ -11,7 +11,6 @@ #import "MVMCoreUISession.h" #import "MVMCoreUISplitViewController.h" #import -@import MVMCore.MVMCoreNavigationHandler; @import MVMCore.MVMCoreGetterUtility; @implementation MVMCoreUIUtility @@ -52,19 +51,10 @@ } + (UIViewController *)getCurrentVisibleController { - UIViewController *baseViewController = [MVMCoreNavigationHandler sharedNavigationHandler].viewControllerToPresentOn ?: [MVMCoreGetterUtility getKeyWindow].rootViewController; - UIViewController *viewController = nil; - while (baseViewController.presentedViewController && !baseViewController.presentedViewController.isBeingDismissed) { - viewController = baseViewController.presentedViewController; - baseViewController = viewController; - } - if ([viewController isKindOfClass:[UINavigationController class]]) { - viewController = ((UINavigationController *)viewController).topViewController; - } - // if it is not presented viewcontroller, existing BAU logic will be working - if (!viewController) { - viewController = [self getViewControllerTraversingManagers:[MVMCoreUISession sharedGlobal].navigationController.topViewController]; - } + __block UIViewController *viewController = nil; + [MVMCoreDispatchUtility performSyncBlockOnMainThread:^{ + viewController = [self getVisibleViewController]; + }]; return viewController; } @@ -150,8 +140,8 @@ if(mdn.length == LengthContactNumber) { NSMutableString * newmdn = [NSMutableString stringWithCapacity:14]; [newmdn appendString:mdn]; - [newmdn insertString:@"." atIndex:6]; - [newmdn insertString:@"." atIndex:3]; + [newmdn insertString:@"-" atIndex:6]; + [newmdn insertString:@"-" atIndex:3]; return newmdn; } diff --git a/Scripts/download_dependencies.sh b/Scripts/download_dependencies.sh index ec05cfd8..ecfdb627 100755 --- a/Scripts/download_dependencies.sh +++ b/Scripts/download_dependencies.sh @@ -22,6 +22,6 @@ fi ./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDS.xcframework" BPHV_MobileFirst_IOS/com/vzw/hss/myverizon/VDS/1.0/VDS-1.0-Debug-SNAPSHOT.zip -./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSColorTokens.xcframework" GVJV_VDS_Maven/@vds-tokens/ios/VDSColorTokens.1.0.6.xcframework.zip +./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSColorTokens.xcframework" GVJV_VDS_Maven/@vds-tokens/ios/VDSColorTokens.2.0.0.xcframework.zip ./Scripts/download_framework.sh $ARTIFACTORY_URL "$FRAMEWORKS_DIR/VDSFormControlsTokens.xcframework" GVJV_VDS_Maven/@vds-tokens/ios/VDSFormControlsTokens.1.0.7.xcframework.zip