further bridging and refactoring of legacy TopAlert to Notification.

This commit is contained in:
Scott Pfeil 2023-06-07 00:34:52 -04:00
parent b5e3f42d6e
commit 928ef15b1f
13 changed files with 210 additions and 112 deletions

View File

@ -21,9 +21,9 @@ open class ActionCollapseNotificationHandler: MVMCoreActionHandlerProtocol {
required public init() {}
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 = await NotificationHandler.shared()?.getCurrentNotification() else { return }
guard let notification = notification.0 as? CollapsableNotificationProtocol else {
NotificationHandler.shared().hideNotification()
NotificationHandler.shared()?.hideNotification()
return
}
await notification.collapse()

View File

@ -9,11 +9,11 @@
import Foundation
import MVMCore
/// Dismiss the current top notification.
/// Dismiss the current notification.
open class ActionDismissNotificationHandler: MVMCoreActionHandlerProtocol {
required public init() {}
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
NotificationHandler.shared().hideNotification()
NotificationHandler.shared()?.hideNotification()
}
}

View File

@ -15,6 +15,6 @@ open class ActionTopNotificationHandler: MVMCoreActionHandlerProtocol {
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionTopNotificationModel else { return }
try await NotificationHandler.shared().showNotification(for: model.topNotification, delegateObject: delegateObject as? MVMCoreUIDelegateObject)
try await NotificationHandler.shared()?.showNotification(for: model.topNotification, delegateObject: delegateObject as? MVMCoreUIDelegateObject)
}
}

View File

@ -60,9 +60,10 @@
// MARK: - Initializer
//--------------------------------------------------
public init(id: String = UUID().uuidString, text: String) {
public init(id: String = UUID().uuidString, text: String, textColor: Color? = nil) {
self.id = id
self.text = text
self.textColor = textColor
}
//--------------------------------------------------

View File

@ -7,6 +7,9 @@
//
import Foundation
import Combine
import Dispatch
import MVMCore
@objcMembers open class CollapsableNotification: View {
//--------------------------------------------------
@ -16,6 +19,9 @@ import Foundation
public let topView = CollapsableNotificationTopView()
public let bottomView = NotificationMoleculeView()
public var verticalStack: UIStackView!
public var cancellables = Set<AnyCancellable>()
private var timerSource: DispatchSourceTimer?
//--------------------------------------------------
// MARK: - Life Cycle
@ -33,6 +39,7 @@ import Foundation
NSLayoutConstraint.constraintPinSubview(verticalStack, pinTop: true, topConstant: 0, pinBottom: true, bottomConstant: 0, pinLeft: true, leftConstant: 0, pinRight: true, rightConstant: 0)
reset()
subscribeForNotifications()
}
open override func updateView(_ size: CGFloat) {
@ -46,6 +53,42 @@ import Foundation
backgroundColor = .mvmGreen()
}
open func subscribeForNotifications() {
// Resets state when about to show.
NotificationHandler.shared()?.onNotificationWillShow.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] (view, _) in
guard let self = self,
self == view else { return }
self.initialState()
}).store(in: &cancellables)
// Begins the collapse timer when shown.
NotificationHandler.shared()?.onNotificationShown.receive(on: DispatchQueue.main)
.sink(receiveValue: { [weak self] (view, _) in
guard let self = self,
self == view,
let model = self.model as? CollapsableNotificationModel else { return }
if !model.initiallyCollapsed {
self.autoCollapse()
}
}).store(in: &cancellables)
// Cancels any collapse timer when dismissing
NotificationHandler.shared()?.onNotificationWillDismiss.sink(receiveValue: { [weak self] (view, _) in
guard let self = self,
self == view else { return }
timerSource?.cancel()
timerSource = nil
}).store(in: &cancellables)
}
/// Set initial collapse/expand state.
public func initialState() {
guard let model = model as? CollapsableNotificationModel else { return }
topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed
topView.button.isUserInteractionEnabled = model.initiallyCollapsed
bottomView.isHidden = model.initiallyCollapsed
verticalStack.layoutIfNeeded()
}
//--------------------------------------------------
// MARK: - Molecule
//--------------------------------------------------
@ -66,16 +109,7 @@ import Foundation
self?.expand(topViewShowing: model.alwaysShowTopLabel)
}
}
// Set initial collapse/expand state.
topView.isHidden = !model.alwaysShowTopLabel && !model.initiallyCollapsed
topView.button.isUserInteractionEnabled = model.initiallyCollapsed
bottomView.isHidden = model.initiallyCollapsed
verticalStack.layoutIfNeeded()
if !model.initiallyCollapsed {
autoCollapse()
}
initialState()
}
open func performBlockOperation(with block: @escaping (MVMCoreBlockOperation) -> Void) {
@ -86,33 +120,39 @@ import Foundation
/// Collapses after a delay
open func autoCollapse() {
let delay: DispatchTimeInterval = DispatchTimeInterval.seconds((model as? CollapsableNotificationModel)?.collapseTime ?? 5)
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
timerSource = DispatchSource.makeTimerSource()
timerSource?.setEventHandler(handler: { [weak self] in
// If accessibility focused, delay collapse.
guard let self = self else { return }
if MVMCoreUIUtility.viewContainsAccessiblityFocus(self) {
NotificationCenter.default.addObserver(self, selector: #selector(self.accessibilityFocusChanged(notification:)), name: UIAccessibility.elementFocusedNotification, object: nil)
} else {
self.collapse()
}
}
self.timerSource = nil
MVMCoreDispatchUtility.performBlock(onMainThread: {
if MVMCoreUIUtility.viewContainsAccessiblityFocus(self) {
NotificationCenter.default.addObserver(self, selector: #selector(self.accessibilityFocusChanged(notification:)), name: UIAccessibility.elementFocusedNotification, object: nil)
} else {
self.collapse()
}
})
})
timerSource?.schedule(deadline: .now() + delay)
timerSource?.activate()
}
/// Collapses to show just the top view.
open func collapse(animated: Bool = true) {
guard !bottomView.isHidden else { return }
performBlockOperation { [weak self] (operation) in
let strongSelf = self
guard let strongSelf = self else { return }
MVMCoreDispatchUtility.performBlock(onMainThread: {
strongSelf?.superview?.superview?.layoutIfNeeded()
strongSelf.superview?.superview?.layoutIfNeeded()
let animation = {
strongSelf?.topView.isHidden = false
strongSelf?.bottomView.isHidden = true
strongSelf?.verticalStack.layoutIfNeeded()
strongSelf.topView.isHidden = false
strongSelf.bottomView.isHidden = true
strongSelf.verticalStack.layoutIfNeeded()
}
let completion: (Bool) -> Void = { (finished) in
strongSelf?.topView.button.isUserInteractionEnabled = true
strongSelf?.superview?.superview?.layoutIfNeeded()
UIAccessibility.post(notification: .layoutChanged, argument: strongSelf?.getAccessibilityLayoutChangedArgument())
strongSelf.topView.button.isUserInteractionEnabled = true
strongSelf.superview?.superview?.layoutIfNeeded()
UIAccessibility.post(notification: .layoutChanged, argument: strongSelf.getAccessibilityLayoutChangedArgument())
operation.markAsFinished()
}
@ -130,19 +170,19 @@ import Foundation
open func expand(topViewShowing: Bool = false, animated: Bool = true) {
guard bottomView.isHidden else { return }
performBlockOperation { [weak self] (operation) in
let strongSelf = self
guard let strongSelf = self else { return }
MVMCoreDispatchUtility.performBlock(onMainThread: {
strongSelf?.superview?.superview?.layoutIfNeeded()
strongSelf?.topView.button.isUserInteractionEnabled = false
strongSelf.superview?.superview?.layoutIfNeeded()
strongSelf.topView.button.isUserInteractionEnabled = false
let animation = {
strongSelf?.topView.isHidden = !topViewShowing
strongSelf?.bottomView.isHidden = false
strongSelf?.verticalStack.layoutIfNeeded()
strongSelf.topView.isHidden = !topViewShowing
strongSelf.bottomView.isHidden = false
strongSelf.verticalStack.layoutIfNeeded()
}
let completion: (Bool) -> Void = { (finished) in
strongSelf?.superview?.superview?.layoutIfNeeded()
UIAccessibility.post(notification: .layoutChanged, argument: strongSelf?.getAccessibilityLayoutChangedArgument())
strongSelf?.autoCollapse()
strongSelf.superview?.superview?.layoutIfNeeded()
UIAccessibility.post(notification: .layoutChanged, argument: strongSelf.getAccessibilityLayoutChangedArgument())
strongSelf.autoCollapse()
operation.markAsFinished()
}

View File

@ -7,6 +7,7 @@
//
import Foundation
import MVMCore
open class CollapsableNotificationModel: NotificationMoleculeModel {
public class override var identifier: String {
@ -17,11 +18,14 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
public var alwaysShowTopLabel = false
public var collapseTime: Int = 5
public var initiallyCollapsed = false
public var pages: [String]?
public init(with topLabel: LabelModel, headline: LabelModel) {
public init(with topLabel: LabelModel, headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, topAction: ActionModelProtocol? = nil, collapseTime: Int? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil) {
self.topLabel = topLabel
super.init(with: headline)
self.topAction = topAction
if let collapseTime = collapseTime {
self.collapseTime = collapseTime
}
super.init(with: headline, style: style, backgroundColor: backgroundColor, body: body, button: button, closeButton: closeButton)
}
open override func setDefaults() {
@ -30,7 +34,12 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
topLabel.numberOfLines = 1
}
if topLabel.textColor == nil {
topLabel.textColor = Color(uiColor: .white)
switch style {
case .error, .warning:
topLabel.textColor = Color(uiColor: .mvmBlack)
default:
topLabel.textColor = Color(uiColor: .mvmWhite)
}
}
if topLabel.textAlignment == nil {
topLabel.textAlignment = .center
@ -44,7 +53,6 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
case alwaysShowTopLabel
case collapseTime
case initiallyCollapsed
case pages
}
required public init(from decoder: Decoder) throws {
@ -60,7 +68,6 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
if let initiallyCollapsed = try typeContainer.decodeIfPresent(Bool.self, forKey: .initiallyCollapsed) {
self.initiallyCollapsed = initiallyCollapsed
}
pages = try typeContainer.decodeIfPresent([String].self, forKey: .pages)
try super.init(from: decoder)
}
@ -73,6 +80,5 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
try container.encode(alwaysShowTopLabel, forKey: .alwaysShowTopLabel)
try container.encode(collapseTime, forKey: .collapseTime)
try container.encode(initiallyCollapsed, forKey: .initiallyCollapsed)
try container.encodeIfPresent(pages, forKey: .pages)
}
}

View File

@ -40,8 +40,13 @@ open class NotificationMoleculeModel: ContainerModel, MoleculeModelProtocol {
// MARK: - Initializer
//--------------------------------------------------
public init(with headline: LabelModel) {
public init(with headline: LabelModel, style: NotificationMoleculeModel.Style = .success, backgroundColor: Color? = nil, body: LabelModel? = nil, button: ButtonModel? = nil, closeButton: NotificationXButtonModel? = nil) {
self.headline = headline
self.style = style
self.backgroundColor = backgroundColor
self.body = body
self.button = button
self.closeButton = closeButton
super.init()
}

View File

@ -7,6 +7,7 @@
//
import Foundation
import MVMCore
public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol {
public static var identifier: String = "notificationXButton"
@ -20,7 +21,10 @@ public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtoco
case action
}
public init() {}
public init(color: Color? = nil, action: ActionModelProtocol = ActionNoopModel()) {
self.color = color
self.action = action
}
public required init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self)

View File

@ -187,7 +187,7 @@ public extension MVMCoreUISplitViewController {
let model = navigationController.getNavigationModel(from: viewController) else { return }
setNavigationBar(for: viewController, navigationController: navigationController, navigationItemModel: model)
Task {
guard (await NotificationHandler.shared().getCurrentNotification()?.0 as? StatusBarUI) == nil else { return }
guard (await NotificationHandler.shared()?.getCurrentNotification()?.0 as? StatusBarUI) == nil || topAlertView == nil else { return }
setStatusBarForCurrentViewController()
}
}
@ -241,21 +241,24 @@ extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol {
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
NotificationHandler.shared()?.onNotificationWillShow.receive(on: DispatchQueue.main)
.sink { [weak self] (notification, model) in
guard let conformer = notification as? StatusBarUI else { return }
let statusBarUI = conformer.getStatusBarUI()
self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style)
}.store(in: &cancellables)
NotificationHandler.shared().onNotificationUpdated.sink { [weak self] (notification, model) in
NotificationHandler.shared()?.onNotificationUpdated.receive(on: DispatchQueue.main)
.sink { [weak self] (notification, model) in
guard let conformer = notification as? StatusBarUI else { return }
let statusBarUI = conformer.getStatusBarUI()
self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style)
}.store(in: &cancellables)
// Ensure the status bar background color and tint are proper for the view controller.
NotificationHandler.shared().onNotificationDismissed.sink { (notification, model) in
guard let conformer = notification as? StatusBarUI else { return }
NotificationHandler.shared()?.onNotificationDismissed.receive(on: DispatchQueue.main)
.sink { (notification, model) in
guard notification is StatusBarUI else { return }
MVMCoreUISplitViewController.main()?.setStatusBarForCurrentViewController()
}.store(in: &cancellables)
self.cancellables = cancellables

View File

@ -106,7 +106,8 @@ extension NotificationContainerView: MVMCoreViewProtocol {
}
public func setupView() {
clipsToBounds = false
translatesAutoresizingMaskIntoConstraints = false
clipsToBounds = true
height.isActive = true
}
}

View File

@ -42,16 +42,16 @@ public class NotificationOperation: MVMCoreOperation {
public var isDisplayable: Bool {
get {
var isDisplayable: Bool = true
displayableQueue.sync {
//displayableQueue.sync {
isDisplayable = _isDisplayable
}
//}
return isDisplayable
}
set {
guard newValue != isDisplayable else { return }
displayableQueue.async(flags: .barrier) { [weak self] in
self?._isDisplayable = newValue
}
//displayableQueue.async(flags: .barrier) { [weak self] in
self._isDisplayable = newValue
//}
}
}
/// Thread safety.
@ -114,13 +114,15 @@ public class NotificationOperation: MVMCoreOperation {
}
public override func main() {
log(message: "Operation Started")
guard !checkAndHandleForCancellation() else { return }
Task {
await withCheckedContinuation { continuation in
// Show the notification.
showTransitionOperation = add(transition: { [weak self] in
guard let self = self else { return }
NotificationHandler.shared().onNotificationWillShow.send((self.notification, self.notificationModel))
NotificationHandler.shared()?.onNotificationWillShow.send((self.notification, self.notificationModel))
self.log(message: "Operation Will Show")
await self.showNotification()
}, completionBlock: {
continuation.resume()
@ -128,12 +130,14 @@ public class NotificationOperation: MVMCoreOperation {
}
guard await properties.getIsDisplayed() else {
// If the animation did not complete...
log(message: "Operation Never Shown")
markAsFinished()
return
}
// Publish that the notification has been shown.
NotificationHandler.shared().onNotificationShown.send((notification, notificationModel))
NotificationHandler.shared()?.onNotificationShown.send((notification, notificationModel))
log(message: "Operation Did Show")
guard !isCancelled else {
// If cancelled during the animation, dismiss immediately.
@ -155,6 +159,8 @@ public class NotificationOperation: MVMCoreOperation {
await withCheckedContinuation({ continuation in
_ = add(transition: { [weak self] in
guard let self = self else { return }
self.log(message: "Operation Will Dismiss")
NotificationHandler.shared()?.onNotificationWillDismiss.send((notification, notificationModel))
await self.hideNotification()
}, completionBlock: {
continuation.resume()
@ -164,16 +170,22 @@ public class NotificationOperation: MVMCoreOperation {
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 }
NotificationHandler.shared()?.onNotificationDismissed.send((notification, notificationModel))
log(message: "Operation Did Dismiss")
guard isCancelled || !notificationModel.persistent else { return }
markAsFinished()
}
}
public override func markAsFinished() {
log(message: "Operation Finished")
super.markAsFinished()
}
public override func cancel() {
super.cancel()
log(message: "Operation Cancelled")
Task {
if await !properties.getIsDisplayed() {
// Cancel any pending show transitions.
@ -190,16 +202,8 @@ public class NotificationOperation: MVMCoreOperation {
}
}
public func copy(with zone: NSZone? = nil) -> Any {
let operation = NotificationOperation(with: notification, notificationModel: notificationModel, transitionDelegate: transitionDelegate)
operation.reAddAfterCancel = reAddAfterCancel
operation.isDisplayable = isDisplayable
for dependency in dependencies {
operation.addDependency(dependency)
}
operation.name = name
operation.qualityOfService = qualityOfService
return operation
public func log(message: String) {
MVMCoreUILoggingHandler.logDebugMessage(withDelegate: "------Notification message: \(message) type: \(notificationModel.type) id: \(notificationModel.id) priority: e\(notificationModel.priority.rawValue) a\(queuePriority.rawValue) operation: \(String(describing: self)) ------")
}
// MARK: - Automatic
@ -212,11 +216,11 @@ public class NotificationOperation: MVMCoreOperation {
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))")
self?.log(message: "Operation Time Out")
guard let self = self,
!self.isFinished,
!self.checkAndHandleForCancellation() else { return }
// If voice over is on and the notification is focused, do not collapse until unfocused.
guard !MVMCoreUIUtility.viewContainsAccessiblityFocus(notification) else {
NotificationCenter.default.addObserver(self, selector: #selector(accessibilityFocusChanged), name: UIAccessibility.elementFocusedNotification, object: nil)
@ -225,15 +229,17 @@ public class NotificationOperation: MVMCoreOperation {
self.stop()
})
timerSource?.setCancelHandler(handler: { [weak self] in
print("SSSS TIMER EVENT CANCELLED FOR: \(String(describing: self?.notificationModel.type))")
self?.log(message: "Operation Time Out Cancel")
})
timerSource?.schedule(deadline: .now() + .seconds(notificationModel.dismissTime))
timerSource?.activate()
}
/// If the voice over user leaves top alert focus, hide.
@objc func accessibilityFocusChanged(_ notification: NSNotification) {
guard let _ = notification.userInfo?[UIAccessibility.focusedElementUserInfoKey],
!MVMCoreUIUtility.viewContainsAccessiblityFocus(self.notification) else { return }
self.log(message: "Operation Accessibility Focus Removed")
NotificationCenter.default.removeObserver(self, name: UIAccessibility.elementFocusedNotification, object: nil)
stop()
}
@ -271,30 +277,54 @@ public class NotificationOperation: MVMCoreOperation {
}
}
extension NotificationOperation: NSCopying {
public func copy(with zone: NSZone? = nil) -> Any {
let operation = NotificationOperation(with: notification, notificationModel: notificationModel, transitionDelegate: transitionDelegate)
operation.reAddAfterCancel = reAddAfterCancel
operation.isDisplayable = isDisplayable
for dependency in dependencies {
operation.addDependency(dependency)
}
operation.name = name
operation.qualityOfService = qualityOfService
return operation
}
}
/// Manages notifications.
public class NotificationHandler {
open class NotificationHandler {
private var queue = OperationQueue()
private var queue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
private var transitionDelegate: NotificationTransitionDelegateProtocol
private var delegateObject: MVMCoreUIDelegateObject?
// MARK: - Publishers
/// Publishes when a notification will show.
public let onNotificationWillShow = PassthroughSubject<(UIView, NotificationModel), Never>()
/// Publishes when a notification is shown.
public let onNotificationShown = PassthroughSubject<(UIView, NotificationModel), Never>()
/// Publishes when a notification will dismissed.
public let onNotificationWillDismiss = PassthroughSubject<(UIView, NotificationModel), Never>()
/// Publishes when a notification is dismissed.
public let onNotificationDismissed = PassthroughSubject<(UIView, NotificationModel), Never>()
/// Publishes when a notification is updated.
public let onNotificationUpdated = PassthroughSubject<(UIView, NotificationModel), Never>()
// MARK: -
/// Returns the handler stored in the CoreUIObject
public static func shared() -> Self {
return MVMCoreActionUtility.fatalClassCheck(object: CoreUIObject.sharedInstance()?.topNotificationHandler)
public static func shared() -> Self? {
guard let shared = CoreUIObject.sharedInstance()?.topNotificationHandler else { return nil }
return MVMCoreActionUtility.fatalClassCheck(object: shared)
}
public init(with transitionDelegate: NotificationTransitionDelegateProtocol) {
@ -316,13 +346,13 @@ public class NotificationHandler {
}
/// Checks for new top alert json
@objc private func responseJSONUpdated(notification: Notification) async {
@objc private func responseJSONUpdated(notification: Notification) {
guard let loadObject = (notification.userInfo?[String(describing: MVMCoreLoadObject.self)] as? MVMCoreLoadObject) else { return }
let delegateObject = loadObject.delegateObject as? MVMCoreUIDelegateObject
// Dismiss any top alerts that server wants us to dismiss.
if let disableType = loadObject.responseInfoMap?.optionalStringForKey("disableType") {
NotificationHandler.shared().cancelNotification(using: { view, model in
NotificationHandler.shared()?.cancelNotification(using: { view, model in
return model.type == disableType
})
}
@ -336,7 +366,7 @@ 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 {
open func showNotification(for json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) async {
do {
let model = try NotificationModel.decode(json: json, delegateObject: delegateObject)
try await showNotification(for: model, delegateObject: delegateObject)
@ -369,7 +399,7 @@ 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: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool {
open func checkAndUpdateExisting(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) -> Bool {
for case let operation as NotificationOperation in queue.operations {
guard operation.notificationModel.type == model.type else { continue }
operation.update(with: model, delegateObject: delegateObject)
@ -389,7 +419,7 @@ public class NotificationHandler {
guard !operation.isCancelled,
!operation.isFinished else { continue }
if operation.isReady,
highestReadyOperation == nil || operation.queuePriority.rawValue > highestReadyOperation!.queuePriority.rawValue {
highestReadyOperation == nil || operation.notificationModel.priority.rawValue > highestReadyOperation!.notificationModel.priority.rawValue {
highestReadyOperation = operation
}
if operation.isExecuting {
@ -417,7 +447,7 @@ public class NotificationHandler {
// MARK: - Verify
/// Returns if any notification is executing
public func isNotificationShowing() -> Bool {
open func isNotificationShowing() -> Bool {
return queue.operations.first(where: { operation in
return operation.isExecuting
}) != nil
@ -426,7 +456,7 @@ public class NotificationHandler {
/** 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 {
open func hasNotification(using predicate: ((UIView, NotificationModel) -> Bool)) -> Bool {
return queue.operations.first(where: { operation in
guard operation.isExecuting,
let operation = operation as? NotificationOperation else { return false }
@ -435,7 +465,7 @@ public class NotificationHandler {
}
/// Returns the current executing notification view and model
public func getCurrentNotification() async -> (UIView, NotificationModel)? {
open func getCurrentNotification() async -> (UIView, NotificationModel)? {
for operation in queue.operations {
guard operation.isExecuting,
let operation = operation as? NotificationOperation,
@ -448,19 +478,25 @@ public class NotificationHandler {
// MARK: - Show and hide
/// Creates the view and queues up the notification.
public func showNotification(for model: NotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) async throws {
open func showNotification(for model: NotificationModel, delegateObject: MVMCoreUIDelegateObject? = nil) async throws {
guard !checkAndUpdateExisting(with: model, delegateObject: delegateObject) else { return }
let view = try await createNotification(with: model, delegateObject: delegateObject)
let operation = NotificationOperation(with: view, notificationModel: model, transitionDelegate: transitionDelegate)
add(operation: operation)
}
@MainActor
private func createNotification(with model: NotificationModel, delegateObject: MVMCoreUIDelegateObject?) async throws -> UIView & MoleculeViewProtocol {
guard let view = ModelRegistry.createMolecule(model.molecule, delegateObject: delegateObject, additionalData: nil) else {
throw ModelRegistry.Error.decoderOther(message: "Molecule not mapped")
}
let operation = NotificationOperation(with: view, notificationModel: model, transitionDelegate: transitionDelegate)
NotificationHandler.shared().add(operation: operation)
return view
}
/// Cancel the current top alert view.
public func hideNotification() {
open func hideNotification() {
guard let currentOperation = queue.operations.first(where: { operation in
return operation.isExecuting
return operation.isExecuting && !operation.isCancelled
}) as? NotificationOperation else { return }
currentOperation.notificationModel.persistent = false
currentOperation.reAddAfterCancel = false
@ -470,7 +506,7 @@ public class NotificationHandler {
/** 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)) {
open func cancelNotification(using predicate: ((UIView, NotificationModel) -> Bool)) {
for case let operation as NotificationOperation in queue.operations {
if predicate(operation.notification, operation.notificationModel) {
operation.reAddAfterCancel = false
@ -480,7 +516,7 @@ public class NotificationHandler {
}
/// Cancel all notifications, current or pending.
public func removeAllNotifications() {
open func removeAllNotifications() {
queue.cancelAllOperations()
}
}
@ -492,14 +528,16 @@ extension NotificationHandler: MVMCorePresentationDelegateProtocol {
let viewController = MVMCoreUIUtility.getViewControllerTraversingManagers(viewController)
guard viewController == MVMCoreUISplitViewController.main()?.getCurrentViewController() else { return }
let pageType = (viewController as? MVMCoreViewControllerProtocol)?.pageType
queue.operations.compactMap {
$0 as? NotificationOperation
}.sorted {
$0.queuePriority.rawValue > $1.queuePriority.rawValue
}.forEach {
$0.updateDisplayable(by: pageType)
Task {
queue.operations.compactMap {
$0 as? NotificationOperation
}.sorted {
$0.notificationModel.priority.rawValue > $1.notificationModel.priority.rawValue
}.forEach {
$0.updateDisplayable(by: pageType)
}
reevaluteQueue()
}
reevaluteQueue()
}
}
@ -510,6 +548,7 @@ extension NotificationOperation {
queuePriority = model.priority
guard isExecuting,
!isCancelled else { return }
self.log(message: "Operation Updated")
updateStopTimer()
Task { @MainActor in
transitionDelegate.update(with: notificationModel, delegateObject: delegateObject)

View File

@ -69,12 +69,14 @@ open class NotificationModel: Codable, Identifiable {
// MARK: - Initializer
//--------------------------------------------------
public init(with type: String, molecule: MoleculeModelProtocol, priority: Operation.QueuePriority = .normal, persistent: Bool = false, dismissTime: Int = 5, pages: [String]? = nil, analyticsData: JSONValueDictionary? = nil, id: String = UUID().uuidString) {
required public init(with type: String, molecule: MoleculeModelProtocol, priority: Operation.QueuePriority = .normal, persistent: Bool = false, dismissTime: Int? = nil, pages: [String]? = nil, analyticsData: JSONValueDictionary? = nil, id: String = UUID().uuidString) {
self.type = type
self.molecule = molecule
self.priority = priority
self.persistent = persistent
self.dismissTime = dismissTime
if let dismissTime = dismissTime {
self.dismissTime = dismissTime
}
self.pages = pages
self.analyticsData = analyticsData
self.id = id

View File

@ -20,9 +20,6 @@ import MVMCore
session = MVMCoreUISession()
Task { @MainActor in
self.sessionHandler = MVMCoreSessionTimeHandler()
let topAlertView = NotificationContainerView()
MVMCoreUISession.sharedGlobal()?.topAlertView = topAlertView
self.topNotificationHandler = NotificationHandler(with: topAlertView)
}
actionHandler = MVMCoreUIActionHandler()
viewControllerMapping = MVMCoreUIViewControllerMappingObject()