further bridging and refactoring of legacy TopAlert to Notification.
This commit is contained in:
parent
b5e3f42d6e
commit
928ef15b1f
@ -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()
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
//--------------------------------------------------
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -106,7 +106,8 @@ extension NotificationContainerView: MVMCoreViewProtocol {
|
||||
}
|
||||
|
||||
public func setupView() {
|
||||
clipsToBounds = false
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
clipsToBounds = true
|
||||
height.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user