diff --git a/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift b/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift index 309ea8f5..b0158e2d 100644 --- a/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift +++ b/MVMCoreUI/Alerts/MVMCoreAlertHandler+Extension.swift @@ -8,88 +8,60 @@ import Foundation -// Temporary, until we can move page checking logic into isReady of topAlertOperation. -@objcMembers public class NotificationPagesOperation: Operation { - public let pages: [String] - - public init(with pages: [String]) { - self.pages = pages +public extension MVMCoreAlertHandler { + + /// Re-evaluates the queue operations + @objc func reevaluteQueue() { + var highestReadyOperation: MVMCoreTopAlertOperation? + var executingOperation: MVMCoreTopAlertOperation? + for case let operation as MVMCoreTopAlertOperation in topAlertQueue.operations { + guard !operation.isCancelled, + !operation.isFinished else { continue } + if operation.isReady, + highestReadyOperation == nil || operation.queuePriority.rawValue > highestReadyOperation!.queuePriority.rawValue { + highestReadyOperation = operation + } + if operation.isExecuting { + executingOperation = operation + } + } + guard let currentOperation = executingOperation else { return } + + // Cancel the executing operation if it is no longer ready to run. Re-add for later if it is persistent. + guard currentOperation.isReady else { + currentOperation.reAddAfterCancel = currentOperation.topAlertObject.persistent + currentOperation.cancel() + return + } + + // If the highest priority operation is not executing, and the executing operation is persistent, cancel it. + if let newOperation = highestReadyOperation, + currentOperation != newOperation, + currentOperation.topAlertObject.persistent { + currentOperation.reAddAfterCancel = true + currentOperation.cancel() + } } - public override var isReady: Bool { - get { - return super.isReady && isCancelled - } + /// Registers to know when pages change. + @objc func registerForPageChanges() { + MVMCoreNavigationHandler.shared()?.addDelegate(self) } } -// TODO: Move this type of logic into isReady for the top alert operation... then can remove this dependency operation. -public extension MVMCoreAlertHandler { - - /// Adds a page type dependency to the operation - @objc func addPagesDependency(to operation: MVMCoreTopAlertOperation) { - // This notification may only show for certain pages. - guard let pages = operation.topAlertObject.json?.optionalArrayForKey("pages") as? [String], - pages.count > 0 else { return } - let pagesOperation = NotificationPagesOperation(with: pages) - pageOperations.addOperation(pagesOperation) - operation.addDependency(pagesOperation) - } - - /// Updates the pages requirement for the operation - @objc func updatePages(for operation: MVMCoreTopAlertOperation, with topAlertObject: MVMCoreTopAlertObject) { - // Add new dependencies, remove old. - let previousPages = operation.dependencies.filter { $0 as? NotificationPagesOperation != nil } - addPagesDependency(to: operation) - for pageOperation in previousPages { - pageOperation.cancel() - operation.removeDependency(pageOperation) - } - } - - /// Handles page dependencies for all top alerts with the new pageType - @objc func handleAllPagesDependency(for pageType: String?) { - var currentOperation: MVMCoreTopAlertOperation? = nil - for case let operation as MVMCoreTopAlertOperation in topAlertQueue.operations { - // Handle the currently executing opration last to avoid race conditions. - guard !operation.isExecuting else { - currentOperation = operation - continue - } - handlePageDependency(for: operation, with: pageType) - } - if let operation = currentOperation { - handlePageDependency(for: operation, with: pageType) - } - } - - /// Updates the operation based on the page type and its dependencies. - @objc func handlePageDependency(for operation: MVMCoreTopAlertOperation, with pageType: String?) { - guard !operation.isCancelled else { return } - - if let pages = operation.topAlertObject.json?.optionalArrayForKey("pages") as? [String], - pages.count > 0, - (pageType == nil || !pages.contains(pageType!)) { - if let dependency = operation.dependencies.first(where: { $0.isFinished && ($0 as? NotificationPagesOperation)?.pages == pages }) { - // Re-add the dependency if it was previously cancelled. - let pagesOperation = NotificationPagesOperation(with: pages) - pageOperations.addOperation(pagesOperation) - operation.addDependency(pagesOperation) - operation.removeDependency(dependency) - } - if operation.isExecuting { - // If the current running operation should not show on the current page, cancel it. If it's persistent, re-add it - operation.reAddAfterCancel = operation.topAlertObject.persistent - operation.cancel() - } - } - - // Cancel any dependency if it contains the current page. - guard let pageType = pageType else { return } - for case let operation as NotificationPagesOperation in operation.dependencies { - if operation.pages.contains(pageType) { - operation.cancel() - } +extension MVMCoreAlertHandler: MVMCorePresentationDelegateProtocol { + // Update displayable for each top alert operation when page type changes, in top queue priority order. + public func navigationController(_ navigationController: UINavigationController, displayedViewController viewController: UIViewController) { + guard navigationController == MVMCoreUISplitViewController.main()?.navigationController else { return } + let pageType = (viewController as? MVMCoreViewControllerProtocol)?.pageType + topAlertQueue.operations.compactMap { + $0 as? MVMCoreTopAlertOperation + }.sorted { + $0.queuePriority.rawValue > $1.queuePriority.rawValue + }.forEach { + $0.updateDisplayable(byPageType: pageType) } + reevaluteQueue() } } + diff --git a/MVMCoreUI/Alerts/MVMCoreAlertHandler.h b/MVMCoreUI/Alerts/MVMCoreAlertHandler.h index f3b30efd..522c5740 100644 --- a/MVMCoreUI/Alerts/MVMCoreAlertHandler.h +++ b/MVMCoreUI/Alerts/MVMCoreAlertHandler.h @@ -23,9 +23,6 @@ // An operation queue for top alerts @property (nonnull, strong, nonatomic) NSOperationQueue *topAlertQueue; -/// Stores any page dependencies for top alerts -@property (nonnull, strong, nonatomic) NSOperationQueue *pageOperations; - /// Returns the shared instance of this singleton + (nullable instancetype)sharedAlertHandler; diff --git a/MVMCoreUI/Alerts/MVMCoreAlertHandler.m b/MVMCoreUI/Alerts/MVMCoreAlertHandler.m index 4e7be1f7..8b4b02c7 100644 --- a/MVMCoreUI/Alerts/MVMCoreAlertHandler.m +++ b/MVMCoreUI/Alerts/MVMCoreAlertHandler.m @@ -14,7 +14,7 @@ #import #import #import -#import +#import @interface MVMCoreAlertHandler () @@ -42,7 +42,7 @@ self.popupAlertQueue.maxConcurrentOperationCount = 1; self.topAlertQueue = [[NSOperationQueue alloc] init]; self.topAlertQueue.maxConcurrentOperationCount = 1; - self.pageOperations = [[NSOperationQueue alloc] init]; + [self registerForPageChanges]; } return self; } @@ -172,17 +172,11 @@ alertOperation = nil; }]; - [self.topAlertQueue addOperation:alertOperation]; + NSString *currentPageType = ((UIViewController *)[[MVMCoreUISplitViewController mainSplitViewController] getCurrentDetailViewController]).pageType; + [alertOperation updateDisplayableByPageType:currentPageType]; - // If the current running operation is persistent and has a lower queue priority then the added operation, cancel it and re-add it. - for (MVMCoreTopAlertOperation *operation in self.topAlertQueue.operations) { - - if (operation.isExecuting && !operation.isCancelled && operation.topAlertObject.persistent && operation.queuePriority < alertOperation.queuePriority && alertOperation.isReady) { - operation.reAddAfterCancel = YES; - [operation cancel]; - break; - } - } + [self.topAlertQueue addOperation:alertOperation]; + [self reevaluteQueue]; } - (void)showTopAlertWithObject:(nullable MVMCoreTopAlertObject *)topAlertObject { diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h b/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h index 73acec1e..af00d789 100644 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h +++ b/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.h @@ -15,6 +15,9 @@ @property (readonly, getter=isPaused) BOOL paused; +/// A bool for if this top alert can be displayed. It is only ready if true. +@property (nonatomic, getter=isDisplayable) BOOL displayable; + @property (nonatomic) BOOL reAddAfterCancel; @property (nonnull, nonatomic, strong) MVMCoreTopAlertObject *topAlertObject; @@ -24,6 +27,9 @@ /// Updates the operation with a new object - (void)updateWithTopAlertObject:(nonnull MVMCoreTopAlertObject *)topAlertObject; +/// Updates the operation isDisplayable based on the page type. +- (void)updateDisplayableByPageType:(nullable NSString *)pageType; + // Pauses the operation. Temporarily removes any alert. - (void)pause; diff --git a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m b/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m index bcf9515a..7a57f989 100644 --- a/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m +++ b/MVMCoreUI/TopAlert/MVMCoreTopAlertOperation.m @@ -10,11 +10,13 @@ #import "MVMCoreTopAlertObject.h" #import "MVMCoreAlertHandler.h" #import +#import @interface MVMCoreTopAlertOperation () { __block BOOL _paused; __block BOOL _displayed; __block BOOL _animating; + __block BOOL _displayable; } @property (readwrite, getter=isPaused) BOOL paused; @@ -25,6 +27,7 @@ @property (strong, nonatomic) dispatch_queue_t pausedQueue; @property (strong, nonatomic) dispatch_queue_t displayedQueue; @property (strong, nonatomic) dispatch_queue_t animatingQueue; +@property (strong, nonatomic) dispatch_queue_t displayableQueue; @property (nonatomic, strong) dispatch_source_t timerSource; @@ -39,6 +42,8 @@ self.pausedQueue = dispatch_queue_create("paused", DISPATCH_QUEUE_CONCURRENT); self.displayedQueue = dispatch_queue_create("displayed", DISPATCH_QUEUE_CONCURRENT); self.animatingQueue = dispatch_queue_create("animating", DISPATCH_QUEUE_CONCURRENT); + self.displayableQueue = dispatch_queue_create("displayable", DISPATCH_QUEUE_CONCURRENT); + self.displayable = YES; } return self; } @@ -66,6 +71,19 @@ } } +- (void)updateDisplayableByPageType:(nullable NSString *)pageType { + NSArray *pages = [self.topAlertObject.json array:@"pages"]; + if (pages.count == 0) { + self.displayable = YES; + return; + } + if (pageType.length == 0) { + self.displayable = NO; + return; + } + self.displayable = [pages containsObject:pageType]; +} + - (BOOL)isPaused { __block BOOL isPaused; dispatch_sync(self.pausedQueue, ^{ @@ -108,6 +126,37 @@ }); } + +- (BOOL)isDisplayable { + __block BOOL isDisplayable; + dispatch_sync(self.displayableQueue, ^{ + isDisplayable = self->_displayable; + }); + return isDisplayable; +} + +- (void)setDisplayable:(BOOL)displayable { + if (displayable != self.isDisplayable) { + BOOL isReady = [super isReady]; + if (isReady) { + [self willChangeValueForKey:@"isReady"]; + } + dispatch_barrier_async(self.displayableQueue, ^{ + self->_displayable = displayable; + }); + if (isReady) { + [self didChangeValueForKey:@"isReady"]; + } + } +} + +- (BOOL)isReady { + if (self.isCancelled) { + return [super isReady]; + } + return [super isReady] && self.isDisplayable; +} + - (void)main { // Always check for cancellation before launching the task. @@ -251,6 +300,7 @@ copyObject.topAlertObject = self.topAlertObject; copyObject.paused = self.paused; copyObject.reAddAfterCancel = self.reAddAfterCancel; + copyObject.displayable = self.isDisplayable; copyObject.queuePriority = self.queuePriority; for (NSOperation *dependency in self.dependencies) { [copyObject addDependency:dependency]; diff --git a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift index 9bceb57a..a0b128b2 100644 --- a/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift +++ b/MVMCoreUI/TopAlert/MVMCoreUITopAlertView+Extension.swift @@ -18,7 +18,6 @@ public extension MVMCoreUITopAlertView { /// Registers with the notification center to know when json is updated. @objc func registerWithNotificationCenter() { NotificationCenter.default.addObserver(self, selector: #selector(responseJSONUpdated(notification:)), name: NSNotification.Name(rawValue: NotificationResponseLoaded), object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(viewControllerChanged(notification:)), name: NSNotification.Name(rawValue: MVMCoreNotificationViewControllerChanged), object: nil) } private func getDelegateObject() -> MVMCoreUIDelegateObject { @@ -42,42 +41,6 @@ public extension MVMCoreUITopAlertView { showTopAlert(with: model) } - /// When a detail page changes, check top alerts. - @objc private func viewControllerChanged(notification: Notification) { - guard let controller = MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol else { return } - MVMCoreAlertHandler.shared()?.handleAllPagesDependency(for: controller.pageType) - reevalutePriority() - } - - /// Re-evaluates the queue priority - private func reevalutePriority() { - guard let operations = MVMCoreAlertHandler.shared()?.topAlertQueue.operations else { return } - var highestReadyOperation: Operation? - var executingOperation: Operation? - for operation in operations { - guard !operation.isCancelled, - !operation.isFinished else { - continue - } - if operation.isReady, - highestReadyOperation == nil || operation.queuePriority.rawValue > highestReadyOperation!.queuePriority.rawValue { - highestReadyOperation = operation - } - if operation.isExecuting { - executingOperation = operation - } - } - - // If the highest priority operation is not executing, and the executing operation is persistent, cancel it. - if let newOperation = highestReadyOperation, - let currentOperation = executingOperation as? MVMCoreTopAlertOperation, - currentOperation != newOperation, - currentOperation.topAlertObject.persistent { - currentOperation.reAddAfterCancel = true - currentOperation.cancel() - } - } - /// Decodes the json into a TopNotificationModel private func decodeTopNotification(with json: [AnyHashable: Any], delegateObject: MVMCoreUIDelegateObject?) -> TopNotificationModel? { do { @@ -95,8 +58,6 @@ public extension MVMCoreUITopAlertView { let object = model.createTopAlertObject() guard !checkAndUpdateExisting(with: object), let operation = MVMCoreTopAlertOperation(topAlertObject: object) else { return } - MVMCoreAlertHandler.shared()?.addPagesDependency(to: operation) - MVMCoreAlertHandler.shared()?.handlePageDependency(for: operation, with: (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType) MVMCoreAlertHandler.shared()?.add(operation) } @@ -107,9 +68,9 @@ public extension MVMCoreUITopAlertView { guard topAlertObject.json != nil, operation.topAlertObject.type == topAlertObject.type else { continue } operation.update(with: topAlertObject) - MVMCoreAlertHandler.shared()?.updatePages(for: operation, with: topAlertObject) - MVMCoreAlertHandler.shared()?.handlePageDependency(for: operation, with: (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType) - reevalutePriority() + let pageType = (MVMCoreUISplitViewController.main()?.getCurrentDetailViewController() as? MVMCoreViewControllerProtocol)?.pageType + operation.updateDisplayable(byPageType: pageType) + MVMCoreAlertHandler.shared()?.reevaluteQueue() return true } return false