diff --git a/MVMCoreUI.xcodeproj/project.pbxproj b/MVMCoreUI.xcodeproj/project.pbxproj index 0f339285..b3225458 100644 --- a/MVMCoreUI.xcodeproj/project.pbxproj +++ b/MVMCoreUI.xcodeproj/project.pbxproj @@ -2033,7 +2033,7 @@ D29DF11921E68467003B2FB9 /* Containers */, D22D1F582204D2590077CEC0 /* Legacy */, D29DF10F21E67A7D003B2FB9 /* BaseControllers */, - D29DF11E21E6851E003B2FB9 /* TopAlert */, + D29DF11E21E6851E003B2FB9 /* TopNotification */, D29DF0CF21E404D4003B2FB9 /* MVMCoreUI.h */, D29DF0D021E404D4003B2FB9 /* Info.plist */, ); @@ -2156,9 +2156,10 @@ path = Containers; sourceTree = ""; }; - D29DF11E21E6851E003B2FB9 /* TopAlert */ = { + D29DF11E21E6851E003B2FB9 /* TopNotification */ = { isa = PBXGroup; children = ( + AFA4932129E5EF2E001A9663 /* TopNotificationHandler.swift */, D2ED2814254B0EE400A1C293 /* MVMCoreGlobalTopAlertDelegateProtocol.h */, D2ED2805254B0EB700A1C293 /* MVMCoreTopAlertAnimationDelegateProtocol.h */, D2ED2809254B0EB700A1C293 /* MVMCoreTopAlertDelegateProtocol.h */, @@ -2180,7 +2181,7 @@ D29DF12721E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.h */, D29DF12121E6851E003B2FB9 /* MVMCoreUITopAlertExpandableView.m */, ); - path = TopAlert; + path = TopNotification; sourceTree = ""; }; D29DF13321E68604003B2FB9 /* Styles */ = { @@ -2531,7 +2532,6 @@ AF7E509729E477C0009DC2AD /* AlertController.swift */, AF7E509629E477C0009DC2AD /* AlertHandler.swift */, AFA4931F29E5CA73001A9663 /* AlertOperation.swift */, - AFA4932129E5EF2E001A9663 /* TopNotificationHandler.swift */, ); path = Alerts; sourceTree = ""; diff --git a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m index 1ef5c232..9dd229ea 100644 --- a/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m +++ b/MVMCoreUI/Containers/SplitViewController/MVMCoreUISplitViewController.m @@ -80,7 +80,7 @@ CGFloat const PanelAnimationDuration = 0.2; return [MVMCoreActionUtility initializerClassCheck:[MVMCoreUISession sharedGlobal].splitViewController classToVerify:self]; } -+ (nullable instancetype)setup:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel { ++ (nullable instancetype)setup:(nullable UIViewController *)leftPanel rightPanel:(nullable UIViewController *)rightPanel topAlertView:(nonnull UIView *)topAlertView { MVMCoreUISplitViewController *splitViewController = [[self alloc] initWithLeftPanel:leftPanel rightPanel:rightPanel]; [MVMCoreUISession sharedGlobal].splitViewController = splitViewController; return splitViewController; diff --git a/MVMCoreUI/OtherHandlers/CoreUIObject.swift b/MVMCoreUI/OtherHandlers/CoreUIObject.swift index 567042ec..0cf9f1d4 100644 --- a/MVMCoreUI/OtherHandlers/CoreUIObject.swift +++ b/MVMCoreUI/OtherHandlers/CoreUIObject.swift @@ -10,7 +10,6 @@ import UIKit import MVMCore @objcMembers open class CoreUIObject: MVMCoreObject { - public var globalTopAlertDelegate: MVMCoreGlobalTopAlertDelegateProtocol? public var alertHandler: AlertHandler? public var topNotificationHandler: TopNotificationHandler? diff --git a/MVMCoreUI/TopAlert/MVMCoreGlobalTopAlertDelegateProtocol.h b/MVMCoreUI/TopNotification/MVMCoreGlobalTopAlertDelegateProtocol.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreGlobalTopAlertDelegateProtocol.h rename to MVMCoreUI/TopNotification/MVMCoreGlobalTopAlertDelegateProtocol.h diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertAnimationDelegateProtocol.h b/MVMCoreUI/TopNotification/MVMCoreTopAlertAnimationDelegateProtocol.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertAnimationDelegateProtocol.h rename to MVMCoreUI/TopNotification/MVMCoreTopAlertAnimationDelegateProtocol.h diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertDelegateProtocol.h b/MVMCoreUI/TopNotification/MVMCoreTopAlertDelegateProtocol.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertDelegateProtocol.h rename to MVMCoreUI/TopNotification/MVMCoreTopAlertDelegateProtocol.h diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.h b/MVMCoreUI/TopNotification/MVMCoreTopAlertObject.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertObject.h rename to MVMCoreUI/TopNotification/MVMCoreTopAlertObject.h diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertObject.m b/MVMCoreUI/TopNotification/MVMCoreTopAlertObject.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertObject.m rename to MVMCoreUI/TopNotification/MVMCoreTopAlertObject.m diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h b/MVMCoreUI/TopNotification/MVMCoreTopAlertOperation.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h rename to MVMCoreUI/TopNotification/MVMCoreTopAlertOperation.h diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m b/MVMCoreUI/TopNotification/MVMCoreTopAlertOperation.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m rename to MVMCoreUI/TopNotification/MVMCoreTopAlertOperation.m diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertViewProtocol.h b/MVMCoreUI/TopNotification/MVMCoreTopAlertViewProtocol.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreTopAlertViewProtocol.h rename to MVMCoreUI/TopNotification/MVMCoreTopAlertViewProtocol.h diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h b/MVMCoreUI/TopNotification/MVMCoreUITopAlertBaseView.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.h rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertBaseView.h diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m b/MVMCoreUI/TopNotification/MVMCoreUITopAlertBaseView.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertBaseView.m rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertBaseView.m diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.h b/MVMCoreUI/TopNotification/MVMCoreUITopAlertExpandableView.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.h rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertExpandableView.h diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m b/MVMCoreUI/TopNotification/MVMCoreUITopAlertExpandableView.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertExpandableView.m rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertExpandableView.m diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.h b/MVMCoreUI/TopNotification/MVMCoreUITopAlertMainView.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.h rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertMainView.h diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m b/MVMCoreUI/TopNotification/MVMCoreUITopAlertMainView.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertMainView.m rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertMainView.m diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.h b/MVMCoreUI/TopNotification/MVMCoreUITopAlertShortView.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.h rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertShortView.h diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.m b/MVMCoreUI/TopNotification/MVMCoreUITopAlertShortView.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertShortView.m rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertShortView.m diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/TopNotification/MVMCoreUITopAlertView+Extension.swift similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertView+Extension.swift diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h b/MVMCoreUI/TopNotification/MVMCoreUITopAlertView.h similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertView.h rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertView.h diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m b/MVMCoreUI/TopNotification/MVMCoreUITopAlertView.m similarity index 100% rename from MVMCoreUI/TopAlert/MVMCoreUITopAlertView.m rename to MVMCoreUI/TopNotification/MVMCoreUITopAlertView.m diff --git a/MVMCoreUI/Alerts/TopNotificationHandler.swift b/MVMCoreUI/TopNotification/TopNotificationHandler.swift similarity index 55% rename from MVMCoreUI/Alerts/TopNotificationHandler.swift rename to MVMCoreUI/TopNotification/TopNotificationHandler.swift index 25a75c7f..7a00568b 100644 --- a/MVMCoreUI/Alerts/TopNotificationHandler.swift +++ b/MVMCoreUI/TopNotification/TopNotificationHandler.swift @@ -7,6 +7,233 @@ // import MVMCore +import Dispatch + +public protocol NotificationTransitionDelegateProtocol { + @MainActor + func show(notification: UIView) async + + @MainActor + func hide(notification: UIView) async + + @MainActor + func update(with model: TopNotificationModel) +} + +public class NotificationOperation: MVMCoreOperation { + + private let notification: UIView + + private var notificationModel: TopNotificationModel + + /// The delegate that manages transitioning the notification. + private let transitionDelegate: NotificationTransitionDelegateProtocol + + /// The notification animation transition operation (show or hide). + private var transitionOperation: MVMCoreOperation? + + /// The stop timer for non-persistent notifications. + private var timerSource: DispatchSourceTimer? + + /// Determines if the operation is ready. Certain notifications are only meant to be displayed on certain pages. + public var isDisplayable: Bool { + get { + var isDisplayable: Bool = true + displayableQueue.sync { + isDisplayable = _isDisplayable + } + return isDisplayable + } + set { + guard newValue != isDisplayable else { return } + displayableQueue.async(flags: .barrier) { [weak self] in + self?._isDisplayable = newValue + } + } + } + private var displayableQueue = DispatchQueue(label: "displayable", attributes: .concurrent) + private var _isDisplayable: Bool = true { + willSet { + guard super.isReady else { return } + willChangeValue(for: \.isReady) + } + didSet { + guard super.isReady else { return } + didChangeValue(for: \.isReady) + } + } + + /// This operation is ready only if this notification is allowed to show. + public override var isReady: Bool { + get { + guard !isCancelled else { return super.isReady } + return super.isReady && isDisplayable + } + } + + private actor Properties { + private var isDisplayed: Bool = false + private var isAnimating: Bool = false + + func set(displayed: Bool) { + isDisplayed = displayed + } + + func getIsDisplayed() -> Bool { + return isDisplayed + } + + func set(animating: Bool) { + isAnimating = animating + } + + func getIsAnimating() -> Bool { + return isAnimating + } + } + private var properties = Properties() + + // A flag for tracking if the operation needs to be re-added because it was cancelled for a higher priority notification. + public var reAddAfterCancel = false + + public init(with notification: UIView, notificationModel: TopNotificationModel, transitionDelegate: NotificationTransitionDelegateProtocol) { + self.notification = notification + self.notificationModel = notificationModel + self.transitionDelegate = transitionDelegate + super.init() + queuePriority = notificationModel.priority + } + + public override func main() { + guard !checkAndHandleForCancellation() else { return } + add { + await self.showNotification() + guard !self.isCancelled else { + // Cancelled, dismiss immediately. + self.stop() + return + } + self.updateStopTimer() + } + } + + public func stop() { + if let timerSource = timerSource { + timerSource.cancel() + } + Task { + guard await properties.getIsDisplayed(), + await !properties.getIsAnimating() else { return } + add { + await self.hideNotification() + guard !self.isCancelled, + !self.notificationModel.persistent else { return } + self.markAsFinished() + } + } + } + + /// Adds the transition of the notification to the queue. + private func add(transition: @escaping () async -> Void) { + Task { + guard await properties.getIsDisplayed(), + await !properties.getIsAnimating() else { return } + transitionOperation = MVMCoreBlockOperation(block: { [weak self] blockOperation in + guard !blockOperation.checkAndHandleForCancellation() else { return } + guard let self = self else { + blockOperation.markAsFinished() + return + } + Task { + await transition() + blockOperation.markAsFinished() + } + }) + transitionOperation?.completionBlock = { [weak self] in + self?.transitionOperation = nil + } + // Add the animation to the navigation queue to avoid animation collisions. + await MVMCoreNavigationHandler.shared()?.addNavigationOperation(transitionOperation!) + } + } + + private func updateStopTimer() { + if let timerSource = timerSource { + timerSource.cancel() + } + guard !notificationModel.persistent else { return } + timerSource = DispatchSource.makeTimerSource() + timerSource?.setEventHandler(handler: { [weak self] in + print("SSSS TIMER EVENT FIRED FOR: \(String(describing: self?.notificationModel.type))") + guard let self = self, + !self.isFinished, + !self.checkAndHandleForCancellation() else { return } + self.stop() + }) + timerSource?.setCancelHandler(handler: { [weak self] in + print("SSSS TIMER EVENT CANCELLED FOR: \(String(describing: self?.notificationModel.type))") + }) + timerSource?.schedule(deadline: .now() + .seconds(notificationModel.dismissTime)) + } + + public override func cancel() { + super.cancel() + Task { + if await !properties.getIsDisplayed() { + // Cancel any pending show transitions. + transitionOperation?.cancel() + } + + // Do nothing if animating. + guard await !properties.getIsAnimating() else { return } + if await properties.getIsDisplayed() { + stop() + } else if isExecuting { + markAsFinished() + } + } + } + + @MainActor + private func showNotification() async { + await properties.set(animating: true) + await transitionDelegate.show(notification: notification) + await properties.set(displayed: true) + await properties.set(animating: false) + } + + @MainActor + private func hideNotification() async { + await properties.set(animating: true) + await transitionDelegate.hide(notification: notification) + await properties.set(displayed: false) + await properties.set(animating: false) + } + + /// Updates the notification with the new model. + public func update(with model: TopNotificationModel) { + self.notificationModel = model + queuePriority = model.priority + guard isExecuting, + !isCancelled else { return } + updateStopTimer() + Task { @MainActor in + transitionDelegate.update(with: notificationModel) + } + } + + func copy(with zone: NSZone? = nil) -> Any { + 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 class TopNotificationHandler { @@ -234,3 +461,10 @@ extension TopNotificationHandler: MVMCorePresentationDelegateProtocol { reevaluteQueue() } } + +extension NotificationOperation { + /// Updates if the operation is displayable based on the page type. + func updateDisplayable(by pageType: String) { + isDisplayable = notificationModel.pages?.contains(pageType) ?? true + } +} diff --git a/MVMCoreUI/TopAlert/TopNotificationModel.swift b/MVMCoreUI/TopNotification/TopNotificationModel.swift similarity index 100% rename from MVMCoreUI/TopAlert/TopNotificationModel.swift rename to MVMCoreUI/TopNotification/TopNotificationModel.swift