Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions JDStatusBarNotification.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -250,6 +252,7 @@
D264F8A21820213200DA0E53 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
D264F8C31820219F00DA0E53 /* SBExampleViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SBExampleViewController.h; sourceTree = "<group>"; };
D264F8C41820219F00DA0E53 /* SBExampleViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SBExampleViewController.m; sourceTree = "<group>"; };
DB8A91C62D86B22100C7F45B /* NotificationQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationQueue.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -386,6 +389,7 @@
28D81AA02B012B0800DE2CDF /* NotificationAnimator.swift */,
28D81A9D2B01257A00DE2CDF /* StyleCache.swift */,
28D81A932B010C7500DE2CDF /* DiscoveryHelper.swift */,
DB8A91C62D86B22100C7F45B /* NotificationQueue.swift */,
);
path = Private;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
);
Expand Down Expand Up @@ -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 */,
);
Expand Down
71 changes: 71 additions & 0 deletions JDStatusBarNotification/Private/NotificationQueue.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
42 changes: 29 additions & 13 deletions JDStatusBarNotification/Public/NotificationPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
43 changes: 27 additions & 16 deletions JDStatusBarNotification/Public/NotificationSwiftUIModifer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down