Notification Swiftify: Name changes and reorganization.

This commit is contained in:
Scott Pfeil 2023-06-02 18:34:11 -04:00
parent 7da3af94ec
commit b5e3f42d6e
16 changed files with 295 additions and 239 deletions

View File

@ -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 = "<group>"; };
D20923582450ECE00044AD09 /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
D20A9A5D2243D3E300ADE781 /* TwoButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwoButtonView.swift; sourceTree = "<group>"; };
D20C7008250BF99B0095B21C /* TopNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopNotificationModel.swift; sourceTree = "<group>"; };
D20C700A250BFDE40095B21C /* MVMCoreUITopAlertView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MVMCoreUITopAlertView+Extension.swift"; sourceTree = "<group>"; };
D20C7008250BF99B0095B21C /* NotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModel.swift; sourceTree = "<group>"; };
D20C700A250BFDE40095B21C /* NotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContainerView.swift; sourceTree = "<group>"; };
D20F3B43252E00E4004B3F56 /* PageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageProtocol.swift; sourceTree = "<group>"; };
D20FB164241A5D75004AFC3A /* NavigationItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationItemModel.swift; sourceTree = "<group>"; };
D213347623843825008E41B3 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = "<group>"; };
@ -954,7 +954,7 @@
D224799A231965AD003FCCF9 /* AccordionMoleculeTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccordionMoleculeTableViewCell.swift; sourceTree = "<group>"; };
D22D8392241C27B100D3DF69 /* BaseTemplateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTemplateModel.swift; sourceTree = "<group>"; };
D22D8394241FB41200D3DF69 /* UIStackView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIStackView+Extension.swift"; sourceTree = "<group>"; };
D23118B225124E18001C8440 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = "<group>"; };
D23118B225124E18001C8440 /* NotificationMoleculeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMoleculeView.swift; sourceTree = "<group>"; };
D2351C7924A4D433007DF0BC /* ListRightVariableToggleAllTextAndLinksModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinksModel.swift; sourceTree = "<group>"; };
D2351C7B24A4D4C3007DF0BC /* ListRightVariableToggleAllTextAndLinks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRightVariableToggleAllTextAndLinks.swift; sourceTree = "<group>"; };
D236E5B2241FEB1000C38625 /* ListTwoColumnPriceDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListTwoColumnPriceDescription.swift; sourceTree = "<group>"; };
@ -1105,7 +1105,7 @@
D2C521A823EDE79E00CA2634 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
D2C78CD124228BBD00B69FDE /* ActionOpenPanelModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionOpenPanelModel.swift; sourceTree = "<group>"; };
D2CAC7CA251104E100C75681 /* NotificationXButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationXButtonModel.swift; sourceTree = "<group>"; };
D2CAC7CC251104FE00C75681 /* NotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModel.swift; sourceTree = "<group>"; };
D2CAC7CC251104FE00C75681 /* NotificationMoleculeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationMoleculeModel.swift; sourceTree = "<group>"; };
D2CAC7CE2511052300C75681 /* CollapsableNotificationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsableNotificationModel.swift; sourceTree = "<group>"; };
D2D2FCEF252B72AF0033EAAA /* MoleculeSectionFooterModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeSectionFooterModel.swift; sourceTree = "<group>"; };
D2D2FCF2252B72CF0033EAAA /* MoleculeSectionFooter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoleculeSectionFooter.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -8,7 +8,7 @@
import Foundation
open class CollapsableNotificationModel: NotificationModel {
open class CollapsableNotificationModel: NotificationMoleculeModel {
public class override var identifier: String {
return "collapsableNotification"
}

View File

@ -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) {

View File

@ -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()

View File

@ -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
}

View File

@ -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<AnyCancellable>()
// 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
}
}

View File

@ -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 <NSNumber *>*tabBarIndices;
@property (nullable, strong, nonatomic) NSSet *cancellables;
// Convenience getter
+ (nullable instancetype)mainSplitViewController;

View File

@ -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 <MVMCoreUIPanelProtocol> *)leftPanel rightPanel:(nullable UIViewController <MVMCoreUIPanelProtocol> *)rightPanel topAlertView:(nullable UIView*)topAlertView {
MVMCoreUISplitViewController *splitViewController = [self setup:leftPanel rightPanel:rightPanel topAlertView:topAlertView];
[[MVMCoreUISession sharedGlobal] setupAsStandardLoadViewDelegate:splitViewController];
if (topAlertView) {
[splitViewController subscribeForNotifications];
}
return splitViewController;
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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)
}