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() {} required public init() {}
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { 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 { guard let notification = notification.0 as? CollapsableNotificationProtocol else {
NotificationHandler.shared().hideNotification() NotificationHandler.shared()?.hideNotification()
return return
} }
await notification.collapse() await notification.collapse()

View File

@ -9,11 +9,11 @@
import Foundation import Foundation
import MVMCore import MVMCore
/// Dismiss the current top notification. /// Dismiss the current notification.
open class ActionDismissNotificationHandler: MVMCoreActionHandlerProtocol { open class ActionDismissNotificationHandler: MVMCoreActionHandlerProtocol {
required public init() {} required public init() {}
open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws { 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 { open func execute(with model: ActionModelProtocol, delegateObject: DelegateObject?, additionalData: [AnyHashable : Any]?) async throws {
guard let model = model as? ActionTopNotificationModel else { return } 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 // 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.id = id
self.text = text self.text = text
self.textColor = textColor
} }
//-------------------------------------------------- //--------------------------------------------------

View File

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

View File

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import MVMCore
open class CollapsableNotificationModel: NotificationMoleculeModel { open class CollapsableNotificationModel: NotificationMoleculeModel {
public class override var identifier: String { public class override var identifier: String {
@ -17,11 +18,14 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
public var alwaysShowTopLabel = false public var alwaysShowTopLabel = false
public var collapseTime: Int = 5 public var collapseTime: Int = 5
public var initiallyCollapsed = false 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 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() { open override func setDefaults() {
@ -30,7 +34,12 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
topLabel.numberOfLines = 1 topLabel.numberOfLines = 1
} }
if topLabel.textColor == nil { 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 { if topLabel.textAlignment == nil {
topLabel.textAlignment = .center topLabel.textAlignment = .center
@ -44,7 +53,6 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
case alwaysShowTopLabel case alwaysShowTopLabel
case collapseTime case collapseTime
case initiallyCollapsed case initiallyCollapsed
case pages
} }
required public init(from decoder: Decoder) throws { 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) { if let initiallyCollapsed = try typeContainer.decodeIfPresent(Bool.self, forKey: .initiallyCollapsed) {
self.initiallyCollapsed = initiallyCollapsed self.initiallyCollapsed = initiallyCollapsed
} }
pages = try typeContainer.decodeIfPresent([String].self, forKey: .pages)
try super.init(from: decoder) try super.init(from: decoder)
} }
@ -73,6 +80,5 @@ open class CollapsableNotificationModel: NotificationMoleculeModel {
try container.encode(alwaysShowTopLabel, forKey: .alwaysShowTopLabel) try container.encode(alwaysShowTopLabel, forKey: .alwaysShowTopLabel)
try container.encode(collapseTime, forKey: .collapseTime) try container.encode(collapseTime, forKey: .collapseTime)
try container.encode(initiallyCollapsed, forKey: .initiallyCollapsed) 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 // 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.headline = headline
self.style = style
self.backgroundColor = backgroundColor
self.body = body
self.button = button
self.closeButton = closeButton
super.init() super.init()
} }

View File

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import MVMCore
public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol { public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtocol {
public static var identifier: String = "notificationXButton" public static var identifier: String = "notificationXButton"
@ -20,7 +21,10 @@ public class NotificationXButtonModel: ButtonModelProtocol, MoleculeModelProtoco
case action 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 { public required init(from decoder: Decoder) throws {
let typeContainer = try decoder.container(keyedBy: CodingKeys.self) 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 } let model = navigationController.getNavigationModel(from: viewController) else { return }
setNavigationBar(for: viewController, navigationController: navigationController, navigationItemModel: model) setNavigationBar(for: viewController, navigationController: navigationController, navigationItemModel: model)
Task { 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() setStatusBarForCurrentViewController()
} }
} }
@ -241,21 +241,24 @@ extension MVMCoreUISplitViewController: MVMCoreViewManagerProtocol {
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
// Ensure the status bar background color and tint are proper for the notification. // 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 } guard let conformer = notification as? StatusBarUI else { return }
let statusBarUI = conformer.getStatusBarUI() let statusBarUI = conformer.getStatusBarUI()
self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style) self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style)
}.store(in: &cancellables) }.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 } guard let conformer = notification as? StatusBarUI else { return }
let statusBarUI = conformer.getStatusBarUI() let statusBarUI = conformer.getStatusBarUI()
self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style) self?.setStatusBarBackgroundColor(statusBarUI.color, style: statusBarUI.style)
}.store(in: &cancellables) }.store(in: &cancellables)
// Ensure the status bar background color and tint are proper for the view controller. // Ensure the status bar background color and tint are proper for the view controller.
NotificationHandler.shared().onNotificationDismissed.sink { (notification, model) in NotificationHandler.shared()?.onNotificationDismissed.receive(on: DispatchQueue.main)
guard let conformer = notification as? StatusBarUI else { return } .sink { (notification, model) in
guard notification is StatusBarUI else { return }
MVMCoreUISplitViewController.main()?.setStatusBarForCurrentViewController() MVMCoreUISplitViewController.main()?.setStatusBarForCurrentViewController()
}.store(in: &cancellables) }.store(in: &cancellables)
self.cancellables = cancellables self.cancellables = cancellables

View File

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

View File

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

View File

@ -69,12 +69,14 @@ open class NotificationModel: Codable, Identifiable {
// MARK: - Initializer // 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.type = type
self.molecule = molecule self.molecule = molecule
self.priority = priority self.priority = priority
self.persistent = persistent self.persistent = persistent
self.dismissTime = dismissTime if let dismissTime = dismissTime {
self.dismissTime = dismissTime
}
self.pages = pages self.pages = pages
self.analyticsData = analyticsData self.analyticsData = analyticsData
self.id = id self.id = id

View File

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