Skip to content

Commit fb96893

Browse files
committed
Add alert-like SwiftUI modfier to present a notification + example, fixes #143
1 parent 8e4faf7 commit fb96893

File tree

6 files changed

+143
-39
lines changed

6 files changed

+143
-39
lines changed

ExampleProject/ExamplesScreen.swift

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ struct ExamplesScreen: View {
2525
@State var showActivity = false
2626
@State var showSubtitle = false
2727
@State var backgroundType: StatusBarNotificationBackgroundType = .pill
28+
@State var showStateDrivenNotification = false
2829

2930
func showDefaultNotification(_ text: String, completion: @escaping (NotificationPresenter) -> ()) {
3031
let styleName = NotificationPresenter.shared.addStyle(named: "default_sample") { style in
@@ -243,6 +244,32 @@ struct ExamplesScreen: View {
243244
NotificationPresenter.shared.dismiss(after: 2.5)
244245
}
245246
}
247+
248+
Button {
249+
showStateDrivenNotification.toggle()
250+
} label: {
251+
Toggle(isOn: $showStateDrivenNotification) {
252+
Text("State driven presentation")
253+
.font(.subheadline)
254+
255+
Text("A .notification() view modifier")
256+
.font(.caption2)
257+
.foregroundColor(.secondary)
258+
}
259+
}
260+
.buttonStyle(PlainButtonStyle())
261+
.notification(isPresented: $showStateDrivenNotification, style: {
262+
$0.backgroundStyle.backgroundColor = .systemTeal
263+
$0.backgroundStyle.pillStyle.minimumWidth = 100.0
264+
}) {
265+
Text("✨ So simple!")
266+
.font(.subheadline)
267+
.bold()
268+
.foregroundStyle(.white)
269+
.lineLimit(0)
270+
.minimumScaleFactor(0.66)
271+
.padding([.leading, .trailing], 20)
272+
}
246273
}
247274

248275
Section("Custom Style Examples") {

JDStatusBarNotification.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
2884CAC02D04DCA5005E00A6 /* NotificationViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2884CABF2D04DCA5005E00A6 /* NotificationViewExtension.swift */; };
11+
2884CAC12D04DCA5005E00A6 /* NotificationViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2884CABF2D04DCA5005E00A6 /* NotificationViewExtension.swift */; };
1012
28D81A942B010C7500DE2CDF /* DiscoveryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D81A932B010C7500DE2CDF /* DiscoveryHelper.swift */; };
1113
28D81A952B0110C700DE2CDF /* DiscoveryHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D81A932B010C7500DE2CDF /* DiscoveryHelper.swift */; };
1214
28D81A9B2B01217100DE2CDF /* NotificationWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28D81A9A2B01217100DE2CDF /* NotificationWindow.swift */; };
@@ -200,6 +202,7 @@
200202
/* End PBXCopyFilesBuildPhase section */
201203

202204
/* Begin PBXFileReference section */
205+
2884CABF2D04DCA5005E00A6 /* NotificationViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewExtension.swift; sourceTree = "<group>"; };
203206
28D81A932B010C7500DE2CDF /* DiscoveryHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHelper.swift; sourceTree = "<group>"; };
204207
28D81A9A2B01217100DE2CDF /* NotificationWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationWindow.swift; sourceTree = "<group>"; };
205208
28D81A9D2B01257A00DE2CDF /* StyleCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StyleCache.swift; sourceTree = "<group>"; };
@@ -369,6 +372,7 @@
369372
7EB914CF2ADC11F4004B3435 /* NotificationPresenter.swift */,
370373
7EF32BB02B10167B000E7CAE /* NotificationPresenterLegacyOverlay.swift */,
371374
28D81AA62B0140C100DE2CDF /* NotificationStyle.swift */,
375+
2884CABF2D04DCA5005E00A6 /* NotificationViewExtension.swift */,
372376
);
373377
path = Public;
374378
sourceTree = "<group>";
@@ -741,6 +745,7 @@
741745
28D81A942B010C7500DE2CDF /* DiscoveryHelper.swift in Sources */,
742746
28D81A9B2B01217100DE2CDF /* NotificationWindow.swift in Sources */,
743747
7E2A4E852AE70A4B001F0DB0 /* NotificationPresenter.swift in Sources */,
748+
2884CAC12D04DCA5005E00A6 /* NotificationViewExtension.swift in Sources */,
744749
28D81AA72B0140C100DE2CDF /* NotificationStyle.swift in Sources */,
745750
7E5402C6286708850079C579 /* JDStatusBarNotification.docc in Sources */,
746751
);
@@ -793,6 +798,7 @@
793798
28D81A9C2B01217100DE2CDF /* NotificationWindow.swift in Sources */,
794799
7EDAEE972AF6EC5E001B6ABE /* NotificationPresenter.swift in Sources */,
795800
28D81A952B0110C700DE2CDF /* DiscoveryHelper.swift in Sources */,
801+
2884CAC02D04DCA5005E00A6 /* NotificationViewExtension.swift in Sources */,
796802
28D81AA82B0140C100DE2CDF /* NotificationStyle.swift in Sources */,
797803
7EDAEEA22AF6EC5E001B6ABE /* JDStatusBarNotification.docc in Sources */,
798804
);

JDStatusBarNotification/Private/NotificationViewController.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
import Foundation
1010
import UIKit
1111

12-
protocol NotificationViewControllerDelegate: NSObject {
13-
func didDismissStatusBar()
12+
protocol NotificationPresentationDelegate: NSObject {
13+
func didPresentNotification()
14+
func didDismissNotification()
1415
}
1516

1617
class NotificationViewController: UIViewController, NotificationViewDelegate {
@@ -24,7 +25,7 @@ class NotificationViewController: UIViewController, NotificationViewDelegate {
2425
private var panMaxY: CGFloat = 0.0
2526

2627
private(set) var statusBarView: NotificationView
27-
weak var delegate: NotificationViewControllerDelegate?
28+
weak var delegate: NotificationPresentationDelegate?
2829
weak var jdsb_window: UIWindow?
2930

3031
init() {
@@ -71,7 +72,10 @@ class NotificationViewController: UIViewController, NotificationViewDelegate {
7172
dismissCompletionBlock = nil
7273

7374
// animate in
74-
animator.animateIn(for: 0.4, completion: completion)
75+
animator.animateIn(for: 0.4) { [weak self] in
76+
self?.delegate?.didPresentNotification()
77+
completion?()
78+
}
7579

7680
return view
7781
}
@@ -215,7 +219,7 @@ class NotificationViewController: UIViewController, NotificationViewDelegate {
215219

216220
// Animate out
217221
animator.animateOut(for: duration) { [weak self] in
218-
self?.delegate?.didDismissStatusBar()
222+
self?.delegate?.didDismissNotification()
219223
completion?()
220224
}
221225
} else {

JDStatusBarNotification/Private/NotificationWindow.swift

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,11 @@
99
import Foundation
1010
import UIKit
1111

12-
protocol NotificationWindowDelegate : AnyObject {
13-
func didDismissStatusBar()
14-
}
15-
16-
class NotificationWindow: UIWindow, NotificationViewControllerDelegate {
12+
class NotificationWindow: UIWindow {
1713
let statusBarViewController: NotificationViewController
18-
weak var delegate: NotificationWindowDelegate?
1914

2015
init(windowScene: UIWindowScene?,
21-
delegate: NotificationWindowDelegate)
16+
delegate: NotificationPresentationDelegate)
2217
{
2318

2419
let statusBarViewController = NotificationViewController()
@@ -36,8 +31,7 @@ class NotificationWindow: UIWindow, NotificationViewControllerDelegate {
3631
super.init(frame: UIScreen.main.bounds)
3732
}
3833

39-
self.delegate = delegate
40-
statusBarViewController.delegate = self
34+
statusBarViewController.delegate = delegate
4135
statusBarViewController.jdsb_window = self
4236
rootViewController = statusBarViewController
4337

@@ -51,12 +45,6 @@ class NotificationWindow: UIWindow, NotificationViewControllerDelegate {
5145
fatalError("init(coder:) has not been implemented")
5246
}
5347

54-
// MARK: - NotificationViewControllerDelegate
55-
56-
func didDismissStatusBar() {
57-
delegate?.didDismissStatusBar()
58-
}
59-
6048
// MARK: - HitTest
6149

6250
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

JDStatusBarNotification/Public/NotificationPresenter.swift

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,29 @@ import SwiftUI
2323
* added by the user also stay in memory permanently.
2424
*/
2525
@objc(JDStatusBarNotificationPresenter)
26-
public class NotificationPresenter: NSObject, NotificationWindowDelegate {
26+
public class NotificationPresenter: NSObject {
2727

2828
var overlayWindow: NotificationWindow?
29-
var styleCache: StyleCache
29+
var windowScene: UIWindowScene?
30+
var styleCache = StyleCache()
3031

31-
/// Provides access to the shared presenter. This is the entry point to present, style and dismiss notifications.
32-
///
33-
/// - Returns: An initialized ``NotificationPresenter`` instance.
34-
@objc(sharedPresenter)
35-
public private(set) static var shared = NotificationPresenter()
32+
// swiftui-alert-style presentation state
33+
var activeNotificationId: UUID? = nil
34+
var didPresentNotificationClosure: ((NotificationPresenter) -> Void)? = nil
35+
var didDismissNotificationClosure: ((NotificationPresenter) -> Void)? = nil
3636

37-
private override init() {
38-
styleCache = StyleCache()
37+
var statusBarView: NotificationView? {
38+
return overlayWindow?.statusBarViewController.statusBarView
3939
}
4040

41+
// keep init private to this file
42+
private override init() {}
43+
}
44+
45+
// MARK: - Core Logic
46+
47+
extension NotificationPresenter : NotificationPresentationDelegate {
48+
4149
/// Called upon animation completion.
4250
///
4351
/// - Parameter presenter: Provides the shared ``NotificationPresenter`` instance. That simplifies any subsequent calls to it upon completion.
@@ -56,6 +64,8 @@ public class NotificationPresenter: NSObject, NotificationWindowDelegate {
5664
subtitle: String? = nil,
5765
style: StatusBarNotificationStyle,
5866
completion: Completion? = nil) -> NotificationView {
67+
activeNotificationId = UUID()
68+
5969
let window = overlayWindow ?? NotificationWindow(windowScene: windowScene, delegate: self)
6070
overlayWindow = window
6171

@@ -72,16 +82,32 @@ public class NotificationPresenter: NSObject, NotificationWindowDelegate {
7282
return view
7383
}
7484

75-
// MARK: - NotificationWindowDelegate
85+
// MARK: - NotificationPresentationDelegate
7686

77-
func didDismissStatusBar() {
87+
func didPresentNotification() {
88+
didPresentNotificationClosure?(self)
89+
}
90+
91+
func didDismissNotification() {
7892
overlayWindow?.removeFromSuperview()
7993
overlayWindow?.isHidden = true
8094
overlayWindow?.rootViewController = nil
8195
overlayWindow = nil
96+
97+
activeNotificationId = nil
98+
didDismissNotificationClosure?(self)
8299
}
100+
}
101+
102+
// MARK: - Public API
83103

84-
// MARK: - Public API
104+
extension NotificationPresenter {
105+
106+
/// Provides access to the shared presenter. This is the entry point to present, style and dismiss notifications.
107+
///
108+
/// - Returns: An initialized ``NotificationPresenter`` instance.
109+
@objc(sharedPresenter)
110+
public private(set) static var shared = NotificationPresenter()
85111

86112
// MARK: - Presentation
87113

@@ -336,13 +362,6 @@ public class NotificationPresenter: NSObject, NotificationWindowDelegate {
336362
public func setWindowScene(_ windowScene: UIWindowScene?) {
337363
self.windowScene = windowScene
338364
}
339-
private var windowScene: UIWindowScene?
340-
341-
// MARK: - Private
342-
343-
private var statusBarView: NotificationView? {
344-
return overlayWindow?.statusBarViewController.statusBarView
345-
}
346365
}
347366

348367
// MARK: - HostingControllerSizingController
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// NotificationViewExtension.swift
3+
// JDStatusBarNotification
4+
//
5+
// Created by Markus on 12/7/24.
6+
// Copyright © 2024 Markus. All rights reserved.
7+
//
8+
9+
import SwiftUI
10+
11+
private struct SwiftUINotficationState {
12+
static var notificationId: UUID? = nil
13+
}
14+
15+
extension View {
16+
public typealias NotificationPrepareStyleClosure = (StatusBarNotificationStyle) -> Void
17+
18+
nonisolated public func notification(isPresented: Binding<Bool>, style: NotificationPrepareStyleClosure? = nil, @ViewBuilder viewBuilder: () -> some View) -> some View {
19+
let np = NotificationPresenter.shared
20+
21+
// dismiss if needed
22+
if !isPresented.wrappedValue && np.isVisible && np.activeNotificationId == SwiftUINotficationState.notificationId {
23+
np.dismiss(animated: true)
24+
}
25+
26+
// present if needed
27+
if isPresented.wrappedValue && SwiftUINotficationState.notificationId == nil {
28+
if let style {
29+
let name = np.addStyle(named: "__swiftui-extension-style") { s in
30+
let _ = style(s)
31+
return s
32+
}
33+
np.presentSwiftView(styleName: name, viewBuilder: viewBuilder)
34+
} else {
35+
np.presentSwiftView(viewBuilder: viewBuilder)
36+
}
37+
38+
// remember id of our presentation
39+
SwiftUINotficationState.notificationId = np.activeNotificationId
40+
41+
// setup callback to react to other calls replacing this presentation
42+
np.didPresentNotificationClosure = {
43+
if $0.activeNotificationId != SwiftUINotficationState.notificationId {
44+
isPresented.wrappedValue = false
45+
SwiftUINotficationState.notificationId = nil
46+
}
47+
}
48+
49+
// reset state on dismissal
50+
np.didDismissNotificationClosure = {
51+
$0.didPresentNotificationClosure = nil
52+
$0.didDismissNotificationClosure = nil
53+
SwiftUINotficationState.notificationId = nil
54+
isPresented.wrappedValue = false
55+
}
56+
}
57+
58+
return self
59+
}
60+
}

0 commit comments

Comments
 (0)