diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 7a83f38e..f0e4f4f6 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -349,8 +349,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 */; }; @@ -369,7 +369,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 */; }; @@ -518,7 +518,7 @@ 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 */; }; D2D2FCF0252B72AF0033EAAA /* MoleculeSectionFooterModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D2FCEF252B72AF0033EAAA /* MoleculeSectionFooterModel.swift */; }; D2D2FCF3252B72CF0033EAAA /* MoleculeSectionFooter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2D2FCF2252B72CF0033EAAA /* MoleculeSectionFooter.swift */; }; @@ -934,8 +934,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 = ""; }; @@ -954,7 +954,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 = ""; }; @@ -1105,7 +1105,7 @@ 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 = ""; }; D2D2FCEF252B72AF0033EAAA /* MoleculeSectionFooterModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeSectionFooterModel.swift; sourceTree = ""; }; D2D2FCF2252B72CF0033EAAA /* MoleculeSectionFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeSectionFooter.swift; sourceTree = ""; }; @@ -2114,8 +2114,8 @@ isa = PBXGroup; children = ( AFA4932129E5EF2E001A9663 /* NotificationHandler.swift */, - D20C7008250BF99B0095B21C /* TopNotificationModel.swift */, - D20C700A250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift */, + D20C7008250BF99B0095B21C /* NotificationModel.swift */, + D20C700A250BFDE40095B21C /* NotificationContainerView.swift */, ); path = Notification; sourceTree = ""; @@ -2421,8 +2421,8 @@ children = ( D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */, D2FA83D12513EA6900564112 /* NotificationXButton.swift */, - D2CAC7CC251104FE00C75681 /* NotificationModel.swift */, - D23118B225124E18001C8440 /* Notification.swift */, + D2CAC7CC251104FE00C75681 /* NotificationMoleculeModel.swift */, + D23118B225124E18001C8440 /* NotificationMoleculeView.swift */, D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */, D2FA83D32514F80C00564112 /* CollapsableNotification.swift */, D2FA83D52515021F00564112 /* CollapsableNotificationTopView.swift */, @@ -2675,7 +2675,7 @@ 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 */, @@ -2816,7 +2816,7 @@ 8DEFA95C243DAC20000D27E5 /* ListThreeColumnDataUsageDividerModel.swift in Sources */, D2092357244FA1EF0044AD09 /* ThreeLayerModelBase.swift in Sources */, D2FD4A4925199BD9000C28A9 /* AccessibilityProtocol.swift in Sources */, - D2CAC7CD251104FE00C75681 /* NotificationModel.swift in Sources */, + D2CAC7CD251104FE00C75681 /* NotificationMoleculeModel.swift in Sources */, 0A1B4A96233BB18F005B3FB4 /* CheckboxLabel.swift in Sources */, EAA0CFAF275E7D8000D65EB0 /* FormFieldEffectProtocol.swift in Sources */, D20923592450ECE00044AD09 /* TableView.swift in Sources */, @@ -2903,7 +2903,7 @@ 32F8804624765C6E00C2ACB3 /* ListLeftVariableNumberedListAllTextAndLinksModel.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 */, @@ -2948,7 +2948,7 @@ 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 */, diff --git a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift index 27cd6e63..449d0c9c 100644 --- a/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift +++ b/MVMCoreUI/Atomic/Actions/ActionCollapseNotificationHandler.swift @@ -12,6 +12,7 @@ import MVMCore /// Notifications that conform are collapsable and can collapse. public protocol CollapsableNotificationProtocol { /// Collapses the notification. + @MainActor func collapse() } @@ -22,9 +23,9 @@ open class ActionCollapseNotificationHandler: MVMCoreActionHandlerProtocol { open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { guard let notification = await NotificationHandler.shared().getCurrentNotification() else { return } guard let notification = notification.0 as? CollapsableNotificationProtocol else { - NotificationHandler.shared().hideTopAlertView() + NotificationHandler.shared().hideNotification() return } - notification.collapse() + await notification.collapse() } } diff --git a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift index e172db9c..34ca66fa 100644 --- a/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift +++ b/MVMCoreUI/Atomic/Actions/ActionDismissNotificationHandler.swift @@ -14,6 +14,6 @@ open class ActionDismissNotificationHandler: MVMCoreActionHandlerProtocol { required public init() {} open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { - NotificationHandler.shared().hideTopAlertView() + NotificationHandler.shared().hideNotification() } } 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/Molecules/TopNotification/CollapsableNotification.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift index eb18f1fd..d91aba51 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotification.swift @@ -14,7 +14,7 @@ import Foundation //-------------------------------------------------- public let topView = CollapsableNotificationTopView() - public let bottomView = NotificationView() + public let bottomView = NotificationMoleculeView() public var verticalStack: UIStackView! //-------------------------------------------------- @@ -170,7 +170,7 @@ import Foundation } 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) @@ -189,6 +189,7 @@ 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..8bcf2569 100644 --- a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift +++ b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationModel.swift @@ -8,7 +8,7 @@ import Foundation -open class CollapsableNotificationModel: NotificationModel { +open class CollapsableNotificationModel: NotificationMoleculeModel { public class override var identifier: String { return "collapsableNotification" } diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/CollapsableNotificationTopView.swift index 04c91450..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 - NotificationView.amendAccesibilityLabel(for: self) + NotificationMoleculeView.amendAccesibilityLabel(for: self) } @objc func pressed(_ sender: Notification) { diff --git a/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift b/MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift similarity index 95% rename from MVMCoreUI/Atomic/Molecules/TopNotification/NotificationModel.swift rename to MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeModel.swift index fe40a022..911ef63b 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,7 +34,7 @@ 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 @@ -132,7 +132,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 89% rename from MVMCoreUI/Atomic/Molecules/TopNotification/Notification.swift rename to MVMCoreUI/Atomic/Molecules/TopNotification/NotificationMoleculeView.swift index f5176396..f05ffc26 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,9 +74,9 @@ import Foundation } open func updateAccessibility() { - NotificationView.amendAccesibilityLabel(for: headline) - NotificationView.amendAccesibilityLabel(for: body) - NotificationView.amendAccesibilityLabel(for: button) + NotificationMoleculeView.amendAccesibilityLabel(for: headline) + NotificationMoleculeView.amendAccesibilityLabel(for: body) + NotificationMoleculeView.amendAccesibilityLabel(for: button) } /// Formats the accessibilityLabel so voice over users know it's in the notification. @@ -88,7 +88,7 @@ import Foundation } } -extension NotificationView: AccessibilityProtocol { +extension NotificationMoleculeView: AccessibilityProtocol { public func getAccessibilityLayoutChangedArgument() -> Any? { return headline } diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift index 764c82ca..3f1562c9 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController+Extension.swift @@ -8,6 +8,14 @@ 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 { @@ -225,3 +233,31 @@ extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol { updateState(with: viewController) } } + +@objc public extension MVMCoreUISplitViewController { + /// 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.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.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.sink { (notification, model) in + guard let conformer = notification as? 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 c41e6860..a5c1577f 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.h @@ -57,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; diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m index 9f7b2aef..a857add7 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m @@ -84,12 +84,18 @@ CGFloat const PanelAnimationDuration = 0.2; 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 topAlertView:(nullable UIView*)topAlertView { MVMCoreUISplitViewController *splitViewController = [self setup:leftPanel rightPanel:rightPanel topAlertView:topAlertView]; [[MVMCoreUISession sharedGlobal] setupAsStandardLoadViewDelegate:splitViewController]; + if (topAlertView) { + [splitViewController subscribeForNotifications]; + } return splitViewController; } diff --git a/MVMCoreUI/Notification/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/Notification/NotificationContainerView.swift similarity index 75% rename from MVMCoreUI/Notification/MVMCoreUITopAlertView+Extension.swift rename to MVMCoreUI/Notification/NotificationContainerView.swift index 29fd7ccd..dfbca4c2 100644 --- a/MVMCoreUI/Notification/MVMCoreUITopAlertView+Extension.swift +++ b/MVMCoreUI/Notification/NotificationContainerView.swift @@ -9,14 +9,10 @@ import Foundation import MVMCore -/// Allows top alerts to determine the status bar color and style. -protocol StatusBarUI { - func getStatusBarUI() -> (color: UIColor, style: UIStatusBarStyle) -} - +/// A simple container view that shows and hides a notification. public class NotificationContainerView: UIView { - public var currentModel: TopNotificationModel? + public var currentModel: NotificationModel? public var currentNotificationView: UIView? lazy private var height = heightAnchor.constraint(equalToConstant: 0) @@ -31,7 +27,8 @@ public class NotificationContainerView: UIView { setupView() } - func updateAccessibilityForTopAlert(_ view: UIView) { + /// 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 { @@ -39,8 +36,6 @@ public class NotificationContainerView: UIView { } UIAccessibility.post(notification: .layoutChanged, argument: accessibilityArgument) } - - // accessibilityFocusChanged; No longer seeing this function, needs a testing. } extension NotificationContainerView: NotificationTransitionDelegateProtocol { @@ -54,11 +49,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol { if let conformer = notification as? MVMCoreViewProtocol { conformer.updateView(bounds.width) } - - if let conformer = notification as? StatusBarUI { - let statusBarUI = conformer.getStatusBarUI() - MVMCoreUISplitViewController.main()?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style) - } superview?.layoutIfNeeded() await withCheckedContinuation { continuation in @@ -83,9 +73,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol { self.superview?.layoutIfNeeded() } completion: { finished in UIAccessibility.post(notification: .layoutChanged, argument: nil) - if let _ = self.currentNotificationView as? StatusBarUI { - MVMCoreUISplitViewController.main()?.setStatusBarForCurrentViewController() - } self.currentNotificationView?.removeFromSuperview() self.currentNotificationView = nil continuation.resume() @@ -94,10 +81,9 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol { } @MainActor - public func update(with model: TopNotificationModel) { + public func update(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) { guard let currentModel = currentModel, currentModel.type == model.type else { return } - let delegateObject = MVMCoreUIDelegateObject.create(withDelegateForAll: self) guard let molecule = currentNotificationView as? MoleculeViewProtocol, currentModel.molecule.moleculeName == model.molecule.moleculeName else { // Log that we couldn't update. @@ -111,11 +97,6 @@ extension NotificationContainerView: NotificationTransitionDelegateProtocol { molecule.reset() molecule.set(with: model.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) } } diff --git a/MVMCoreUI/Notification/NotificationHandler.swift b/MVMCoreUI/Notification/NotificationHandler.swift index 1040a6b2..43047543 100644 --- a/MVMCoreUI/Notification/NotificationHandler.swift +++ b/MVMCoreUI/Notification/NotificationHandler.swift @@ -10,6 +10,7 @@ import MVMCore import Dispatch import Combine +/// Handles the UI tasks for the notification. public protocol NotificationTransitionDelegateProtocol { @MainActor func show(notification: UIView) async @@ -18,25 +19,26 @@ public protocol NotificationTransitionDelegateProtocol { func hide(notification: UIView) async @MainActor - func update(with model: TopNotificationModel) + 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: TopNotificationModel + public var notificationModel: NotificationModel /// The delegate that manages transitioning the notification. private let transitionDelegate: NotificationTransitionDelegateProtocol - /// The notification animation transition operation (show or hide). - private var transitionOperation: MVMCoreOperation? - + /// 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. Certain notifications are only meant to be displayed on certain pages. + /// 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 @@ -52,7 +54,9 @@ public class NotificationOperation: MVMCoreOperation { } } } + /// 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 } @@ -73,7 +77,10 @@ public class NotificationOperation: MVMCoreOperation { } 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) { @@ -92,12 +99,13 @@ public class NotificationOperation: MVMCoreOperation { 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. + /// 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: TopNotificationModel, transitionDelegate: NotificationTransitionDelegateProtocol) { + public init(with notification: UIView, notificationModel: NotificationModel, transitionDelegate: NotificationTransitionDelegateProtocol) { self.notification = notification self.notificationModel = notificationModel self.transitionDelegate = transitionDelegate @@ -107,15 +115,32 @@ public class NotificationOperation: MVMCoreOperation { public override func main() { guard !checkAndHandleForCancellation() else { return } - add { [weak self] in - guard let self = self else { return } - await self.showNotification() - guard !self.isCancelled else { - // Cancelled, dismiss immediately. - self.stop() + 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)) + await self.showNotification() + }, completionBlock: { + continuation.resume() + }) + } + guard await properties.getIsDisplayed() else { + // If the animation did not complete... + markAsFinished() return } - self.updateStopTimer() + + // Publish that the notification has been shown. + NotificationHandler.shared().onNotificationShown.send((notification, notificationModel)) + + guard !isCancelled else { + // If cancelled during the animation, dismiss immediately. + stop() + return + } + updateStopTimer() } } @@ -126,69 +151,33 @@ public class NotificationOperation: MVMCoreOperation { Task { guard await properties.getIsDisplayed(), await !properties.getIsAnimating() else { return } - add { [weak self] in - guard let self = self else { return } - await self.hideNotification() - guard !self.isCancelled, - !self.notificationModel.persistent else { return } - self.markAsFinished() - } - } - } - - /// Adds the transition of the notification to the queue. - private func add(transition: @escaping () async -> Void) { - Task { - guard await properties.getIsDisplayed(), - await !properties.getIsAnimating() else { return } - transitionOperation = MVMCoreBlockOperation(block: { blockOperation in - guard !blockOperation.checkAndHandleForCancellation() else { return } - Task { - await transition() - blockOperation.markAsFinished() - } + // Hide the notification + await withCheckedContinuation({ continuation in + _ = add(transition: { [weak self] in + guard let self = self else { return } + await self.hideNotification() + }, completionBlock: { + continuation.resume() + }) }) - transitionOperation?.completionBlock = { [weak self] in - self?.transitionOperation = nil - } - // Add the animation to the navigation queue to avoid animation collisions. - await MVMCoreNavigationHandler.shared()?.addNavigationOperation(transitionOperation!) + // The animation must complete... + guard await !self.properties.getIsDisplayed() else { return } + + // Publish that the notification has been hidden. + NotificationHandler.shared().onNotificationDismissed.send((notification, notificationModel)) + + guard !isCancelled, + !notificationModel.persistent else { return } + markAsFinished() } } - private func updateStopTimer() { - if let timerSource = timerSource { - timerSource.cancel() - } - guard !notificationModel.persistent else { return } - timerSource = DispatchSource.makeTimerSource() - timerSource?.setEventHandler(handler: { [weak self] in - print("SSSS TIMER EVENT FIRED FOR: \(String(describing: self?.notificationModel.type))") - guard let self = self, - !self.isFinished, - !self.checkAndHandleForCancellation() else { return } - /* - // 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; - } - */ - self.stop() - }) - timerSource?.setCancelHandler(handler: { [weak self] in - print("SSSS TIMER EVENT CANCELLED FOR: \(String(describing: self?.notificationModel.type))") - }) - timerSource?.schedule(deadline: .now() + .seconds(notificationModel.dismissTime)) - } - public override func cancel() { super.cancel() Task { if await !properties.getIsDisplayed() { // Cancel any pending show transitions. - transitionOperation?.cancel() + showTransitionOperation?.cancel() } // Do nothing if animating. @@ -201,37 +190,7 @@ public class NotificationOperation: MVMCoreOperation { } } - @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) - NotificationHandler.shared().onNotificationShown.send((notification, notificationModel)) - } - - @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) - NotificationHandler.shared().onNotificationDismissed.send((notification, notificationModel)) - } - - /// Updates the notification with the new model. - public func update(with model: TopNotificationModel) { - self.notificationModel = model - queuePriority = model.priority - guard isExecuting, - !isCancelled else { return } - updateStopTimer() - Task { @MainActor in - transitionDelegate.update(with: notificationModel) - } - } - - func copy(with zone: NSZone? = nil) -> Any { + public func copy(with zone: NSZone? = nil) -> Any { let operation = NotificationOperation(with: notification, notificationModel: notificationModel, transitionDelegate: transitionDelegate) operation.reAddAfterCancel = reAddAfterCancel operation.isDisplayable = isDisplayable @@ -242,22 +201,96 @@ public class NotificationOperation: MVMCoreOperation { operation.qualityOfService = qualityOfService return operation } + + // 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 + print("SSSS TIMER EVENT FIRED FOR: \(String(describing: self?.notificationModel.type))") + 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) + return + } + self.stop() + }) + timerSource?.setCancelHandler(handler: { [weak self] in + print("SSSS TIMER EVENT CANCELLED FOR: \(String(describing: self?.notificationModel.type))") + }) + timerSource?.schedule(deadline: .now() + .seconds(notificationModel.dismissTime)) + } + + /// If the voice over user leaves top alert focus, hide. + @objc func accessibilityFocusChanged(_ notification: NSNotification) { + guard let _ = notification.userInfo?[UIAccessibility.focusedElementUserInfoKey], + !MVMCoreUIUtility.viewContainsAccessiblityFocus(self.notification) else { return } + NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, 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 + MVMCoreNavigationHandler.shared()?.addNavigationOperation(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) + } } +/// Manages notifications. public class NotificationHandler { - /// The operation queue of top notification operations. private var queue = OperationQueue() - public var transitionDelegate: NotificationTransitionDelegateProtocol + private var transitionDelegate: NotificationTransitionDelegateProtocol private var delegateObject: MVMCoreUIDelegateObject? + /// Publishes when a notification will show. + public let onNotificationWillShow = PassthroughSubject<(UIView, NotificationModel), Never>() + /// Publishes when a notification is shown. - public let onNotificationShown = PassthroughSubject<(UIView, TopNotificationModel), Never>() + public let onNotificationShown = PassthroughSubject<(UIView, NotificationModel), Never>() /// Publishes when a notification is dismissed. - public let onNotificationDismissed = PassthroughSubject<(UIView, TopNotificationModel), Never>() + public let onNotificationDismissed = PassthroughSubject<(UIView, NotificationModel), Never>() + + /// Publishes when a notification is updated. + public let onNotificationUpdated = PassthroughSubject<(UIView, NotificationModel), Never>() /// Returns the handler stored in the CoreUIObject public static func shared() -> Self { @@ -285,10 +318,13 @@ public class NotificationHandler { /// Checks for new top alert json @objc private func responseJSONUpdated(notification: Notification) async { 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/ + // Dismiss any top alerts that server wants us to dismiss. if let disableType = loadObject.responseInfoMap?.optionalStringForKey("disableType") { - NotificationHandler.shared().hideTopAlertView(of: disableType) + NotificationHandler.shared().cancelNotification(using: { view, model in + return model.type == disableType + }) } // Show any new top alert. @@ -299,9 +335,10 @@ public class NotificationHandler { } } + /// Converts the json to a model and creates the view and queues up the notification. public func showNotification(for json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) async { do { - let model = try TopNotificationModel.decode(json: json, delegateObject: delegateObject) + let model = try NotificationModel.decode(json: json, delegateObject: delegateObject) try await showNotification(for: model, delegateObject: delegateObject) } catch { if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: "\(self)") { @@ -332,10 +369,10 @@ public class NotificationHandler { } /// 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 model: TopNotificationModel) -> Bool { + private 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) + operation.update(with: model, delegateObject: delegateObject) let pageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType operation.updateDisplayable(by: pageType) reevaluteQueue() @@ -377,76 +414,28 @@ public class NotificationHandler { } } - // MARK: - Show and hide - - public func isTopAlertShowing() -> Bool { + // MARK: - Verify + + /// Returns if any notification is executing + public func isNotificationShowing() -> Bool { return queue.operations.first(where: { operation in return operation.isExecuting }) != nil } - public func hasPersistentTopAlert(of type: String) -> Bool { + /** Returns if the first executing operation matches the provided predicate. + * @param predicate The predicate block to decide if it is the notification. + */ + public 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 operation.notificationModel.persistent && operation.notificationModel.type == type + return predicate(operation.notification, operation.notificationModel) }) as? NotificationOperation != nil } - /// Creates the view and queues up the notification. - public func showNotification(for model: TopNotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) async throws { - guard !checkAndUpdateExisting(with: model) else { return } - let view = try await createMolecule(for: model, delegateObject: delegateObject) - let operation = NotificationOperation(with: view, notificationModel: model, transitionDelegate: transitionDelegate) - NotificationHandler.shared().add(operation: operation) - } - - /// Cancel the current top alert view. - public func hideTopAlertView() { - guard let currentOperation = queue.operations.first(where: { operation in - return operation.isExecuting - }) as? NotificationOperation else { return } - currentOperation.notificationModel.persistent = false - currentOperation.reAddAfterCancel = false - currentOperation.cancel() - } - - /// Cancel all operations of this type. - public func hideTopAlertView(of type: String) { - for operation in queue.operations { - guard let operation = operation as? NotificationOperation, - operation.notificationModel.type == type else { continue } - operation.reAddAfterCancel = false - operation.cancel() - } - } - - /// Cancel all persistent operations of this type. - public func hidePersistentTopAlertView(of type: String) { - for operation in queue.operations { - guard let operation = operation as? NotificationOperation, - operation.notificationModel.persistent, - operation.notificationModel.type == type else { continue } - operation.reAddAfterCancel = false - operation.cancel() - } - } - - /// Finds an cancels top alerts associated with the object. - public func removeTopAlert(for object: TopNotificationModel) { - for operation in queue.operations { - guard let operation = operation as? NotificationOperation, - operation.notificationModel.id == object.id else { return } - operation.reAddAfterCancel = false - operation.cancel() - } - } - - public func removeAllTopAlerts() { - queue.cancelAllOperations() - } - - public func getCurrentNotification() async -> (UIView, TopNotificationModel)? { + /// Returns the current executing notification view and model + public func getCurrentNotification() async -> (UIView, NotificationModel)? { for operation in queue.operations { guard operation.isExecuting, let operation = operation as? NotificationOperation, @@ -456,15 +445,43 @@ public class NotificationHandler { return nil } - /// Creates and returns the molecule view. - @MainActor - private func createMolecule(for model: TopNotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) throws -> UIView { - do { - guard let molecule = ModelRegistry.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else { - throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped") - } - return molecule + // MARK: - Show and hide + + /// Creates the view and queues up the notification. + public func showNotification(for model: NotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) async throws { + guard !checkAndUpdateExisting(with: model, delegateObject: delegateObject) else { return } + guard let view = ModelRegistry.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else { + throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped") } + let operation = NotificationOperation(with: view, notificationModel: model, transitionDelegate: transitionDelegate) + NotificationHandler.shared().add(operation: operation) + } + + /// Cancel the current top alert view. + public func hideNotification() { + guard let currentOperation = queue.operations.first(where: { operation in + return operation.isExecuting + }) 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. + */ + public 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. + public func removeAllNotifications() { + queue.cancelAllOperations() } } @@ -487,6 +504,18 @@ extension NotificationHandler: MVMCorePresentationDelegateProtocol { } 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 } + updateStopTimer() + Task { @MainActor in + transitionDelegate.update(with: notificationModel, delegateObject: delegateObject) + } + } + /// Updates if the operation is displayable based on the page type. func updateDisplayable(by pageType: String?) { guard let pages = notificationModel.pages else { diff --git a/MVMCoreUI/Notification/TopNotificationModel.swift b/MVMCoreUI/Notification/NotificationModel.swift similarity index 98% rename from MVMCoreUI/Notification/TopNotificationModel.swift rename to MVMCoreUI/Notification/NotificationModel.swift index 9e84c8b9..2de77988 100644 --- a/MVMCoreUI/Notification/TopNotificationModel.swift +++ b/MVMCoreUI/Notification/NotificationModel.swift @@ -1,5 +1,5 @@ // -// TopNotification.swift +// NotificationModel.swift // MVMCoreUI // // Created by Scott Pfeil on 9/11/20. @@ -9,7 +9,7 @@ import Foundation import MVMCore -open class TopNotificationModel: Codable, Identifiable { +open class NotificationModel: Codable, Identifiable { public var type: String public var priority = Operation.QueuePriority.normal public var molecule: MoleculeModelProtocol diff --git a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift index 1a109624..af356d93 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIModelMapping.swift @@ -209,7 +209,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) }