diff --git a/JDStatusBarNotification.xcodeproj/project.pbxproj b/JDStatusBarNotification.xcodeproj/project.pbxproj index 9b6be7a2..7dec489a 100644 --- a/JDStatusBarNotification.xcodeproj/project.pbxproj +++ b/JDStatusBarNotification.xcodeproj/project.pbxproj @@ -103,6 +103,8 @@ D264F8A11820213200DA0E53 /* SBAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D264F8A01820213200DA0E53 /* SBAppDelegate.m */; }; D264F8A31820213200DA0E53 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D264F8A21820213200DA0E53 /* Images.xcassets */; }; D264F8C61820219F00DA0E53 /* SBExampleViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D264F8C41820219F00DA0E53 /* SBExampleViewController.m */; }; + DB8A91C72D86B22100C7F45B /* NotificationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8A91C62D86B22100C7F45B /* NotificationQueue.swift */; }; + DB8A91C82D86B22100C7F45B /* NotificationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8A91C62D86B22100C7F45B /* NotificationQueue.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -250,6 +252,7 @@ D264F8A21820213200DA0E53 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; D264F8C31820219F00DA0E53 /* SBExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBExampleViewController.h; sourceTree = ""; }; D264F8C41820219F00DA0E53 /* SBExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBExampleViewController.m; sourceTree = ""; }; + DB8A91C62D86B22100C7F45B /* NotificationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationQueue.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -386,6 +389,7 @@ 28D81AA02B012B0800DE2CDF /* NotificationAnimator.swift */, 28D81A9D2B01257A00DE2CDF /* StyleCache.swift */, 28D81A932B010C7500DE2CDF /* DiscoveryHelper.swift */, + DB8A91C62D86B22100C7F45B /* NotificationQueue.swift */, ); path = Private; sourceTree = ""; @@ -746,6 +750,7 @@ 28D81A9B2B01217100DE2CDF /* NotificationWindow.swift in Sources */, 7E2A4E852AE70A4B001F0DB0 /* NotificationPresenter.swift in Sources */, 2884CAC12D04DCA5005E00A6 /* NotificationSwiftUIModifer.swift in Sources */, + DB8A91C72D86B22100C7F45B /* NotificationQueue.swift in Sources */, 28D81AA72B0140C100DE2CDF /* NotificationStyle.swift in Sources */, 7E5402C6286708850079C579 /* JDStatusBarNotification.docc in Sources */, ); @@ -799,6 +804,7 @@ 7EDAEE972AF6EC5E001B6ABE /* NotificationPresenter.swift in Sources */, 28D81A952B0110C700DE2CDF /* DiscoveryHelper.swift in Sources */, 2884CAC02D04DCA5005E00A6 /* NotificationSwiftUIModifer.swift in Sources */, + DB8A91C82D86B22100C7F45B /* NotificationQueue.swift in Sources */, 28D81AA82B0140C100DE2CDF /* NotificationStyle.swift in Sources */, 7EDAEEA22AF6EC5E001B6ABE /* JDStatusBarNotification.docc in Sources */, ); diff --git a/JDStatusBarNotification/Private/NotificationQueue.swift b/JDStatusBarNotification/Private/NotificationQueue.swift new file mode 100644 index 00000000..6d3b776c --- /dev/null +++ b/JDStatusBarNotification/Private/NotificationQueue.swift @@ -0,0 +1,71 @@ +// +// NotificationQueue.swift +// JDStatusBarNotification +// +// Created by P Deepanshu on 16/03/25. +// Copyright © 2025 Markus. All rights reserved. +// + +import Foundation +import JDStatusBarNotification + +/// A notification item that represent a pending notification +struct QueuedNotification { + let id: UUID + let title: String? + let subtitle: String? + let style: StatusBarNotificationStyle + let duration: Double? + let completion: NotificationPresenter.Completion? +} + +// Manage a queue of notification +class NotificationQueue { + private var queue: [QueuedNotification] = [] + private var isPresenting = false + private weak var presenter: NotificationPresenter? + + init(presenter: NotificationPresenter) { + self.presenter = presenter + } + + // Adds a notification in the queue + func enqueue(_ notification: QueuedNotification) { + queue.append(notification) + processNextNotification() + } + + private func processNextNotification() { + guard !isPresenting, let notification = queue.first, let presenter = presenter else { return } + isPresenting = true + + // Register a custom style for this notification + let styleName = "queue-notification-\(notification.id)" + presenter.addStyle(named: styleName, usingStyle: .defaultStyle) { _ in + return notification.style + } + + // Present the notification + let view = presenter.present(notification.title ?? "", + subtitle: notification.subtitle, + styleName: styleName, + duration: notification.duration) { [weak self] (presenter: NotificationPresenter) in + notification.completion?(presenter) + self?.notificationDismissed() + } + } + + // Called when a notification is dismissed + func notificationDismissed() { + guard !queue.isEmpty else { return } + queue.removeFirst() + isPresenting = false + if !queue.isEmpty { + processNextNotification() + } + } + + func clear() { + queue.removeAll() + } +} diff --git a/JDStatusBarNotification/Public/NotificationPresenter.swift b/JDStatusBarNotification/Public/NotificationPresenter.swift index fc97ae2f..0ccd33b3 100644 --- a/JDStatusBarNotification/Public/NotificationPresenter.swift +++ b/JDStatusBarNotification/Public/NotificationPresenter.swift @@ -28,6 +28,7 @@ public class NotificationPresenter: NSObject { var overlayWindow: NotificationWindow? var windowScene: UIWindowScene? var styleCache = StyleCache() + private var notificationQueue: NotificationQueue! // swiftui-alert-style presentation state var activeNotificationId: UUID? = nil @@ -39,7 +40,10 @@ public class NotificationPresenter: NSObject { } // keep init private to this file - private override init() {} + private override init() { + super.init() + notificationQueue = NotificationQueue(presenter: self) + } } // MARK: - Core Logic @@ -131,11 +135,16 @@ extension NotificationPresenter { completion: Completion? = nil) -> UIView { let style = styleCache.style(forName: styleName) - let view = present(title, subtitle: subtitle, style: style, completion: completion) - if let duration { - dismiss(after: duration) - } - return view + let notification = QueuedNotification( + id: UUID(), + title: title, + subtitle: subtitle, + style: style, + duration: duration, + completion: completion + ) + notificationQueue.enqueue(notification) + return statusBarView ?? UIView() } /// Present a notification using an included style. @@ -157,11 +166,16 @@ extension NotificationPresenter { completion: Completion? = nil) -> UIView { let style = styleCache.style(forIncludedStyle: includedStyle) - let view = present(title, subtitle: subtitle, style: style, completion: completion) - if let duration { - dismiss(after: duration) - } - return view + let notification = QueuedNotification( + id: UUID(), + title: title, + subtitle: subtitle, + style: style, + duration: duration, + completion: completion + ) + notificationQueue.enqueue(notification) + return statusBarView ?? UIView() } /// Present a notification using a custom subview. @@ -236,9 +250,11 @@ extension NotificationPresenter { /// - completion: A ``Completion`` closure, which gets called once the dismiss animation finishes. /// public func dismiss(animated: Bool = true, after delay: Double? = nil, completion: Completion? = nil) { - overlayWindow?.statusBarViewController.dismiss(withDuration: animated ? 0.4 : 0.0, afterDelay: delay ?? 0.0, completion: { + overlayWindow?.statusBarViewController.dismiss(withDuration: animated ? 0.4 : 0.0, afterDelay: delay ?? 0.0) { [weak self] in + guard let self = self else { return } completion?(self) - }) + self.notificationQueue.notificationDismissed() + } } // MARK: - Style Customization diff --git a/JDStatusBarNotification/Public/NotificationSwiftUIModifer.swift b/JDStatusBarNotification/Public/NotificationSwiftUIModifer.swift index 3f7300cd..7c2334ae 100644 --- a/JDStatusBarNotification/Public/NotificationSwiftUIModifer.swift +++ b/JDStatusBarNotification/Public/NotificationSwiftUIModifer.swift @@ -201,22 +201,33 @@ extension View { // present if needed if isPresented.wrappedValue && SwiftUINotficationState.notificationId == nil { + SwiftUINotficationState.notificationId = UUID() + if let includedStyle { - np.present(title, subtitle: subtitle, includedStyle: includedStyle) + np.present(title, subtitle: subtitle, includedStyle: includedStyle) { [id = SwiftUINotficationState.notificationId] presenter in + if presenter.activeNotificationId != id { + isPresented.wrappedValue = false + SwiftUINotficationState.notificationId = nil + } + } } else { - np.present(title, subtitle: subtitle, styleName: styleName) + np.present(title, subtitle: subtitle, styleName: styleName) { [id = SwiftUINotficationState.notificationId] presenter in + if presenter.activeNotificationId != id { + isPresented.wrappedValue = false + SwiftUINotficationState.notificationId = nil + } + } } - trackNotificationState(isPresented: isPresented) - } - // update activity - if let isShowingActivity { - np.displayActivityIndicator(isShowingActivity.wrappedValue) - } + // update activity + if let isShowingActivity { + np.displayActivityIndicator(isShowingActivity.wrappedValue) + } - // update progress bar - if let progress { - np.displayProgressBar(at: progress.wrappedValue) + // update progress bar + if let progress { + np.displayProgressBar(at: progress.wrappedValue) + } } return self @@ -229,17 +240,17 @@ extension View { SwiftUINotficationState.notificationId = np.activeNotificationId // setup callback to react to other calls replacing this presentation - np.didPresentNotificationClosure = { - if $0.activeNotificationId != SwiftUINotficationState.notificationId { + np.didPresentNotificationClosure = { [id = SwiftUINotficationState.notificationId] presenter in + if presenter.activeNotificationId != id { isPresented.wrappedValue = false SwiftUINotficationState.notificationId = nil } } // reset state on dismissal - np.didDismissNotificationClosure = { - $0.didPresentNotificationClosure = nil - $0.didDismissNotificationClosure = nil + np.didDismissNotificationClosure = { presenter in + presenter.didPresentNotificationClosure = nil + presenter.didDismissNotificationClosure = nil SwiftUINotficationState.notificationId = nil isPresented.wrappedValue = false }