Skip to content

Commit 9b5d3e6

Browse files
committed
✨ feat: add onTap parameter for notification click handling
- Add optional onTap closure parameter to present() method - Wrap notification view in Button when onTap is provided - Create ClickableHostingView to support clicks in non-key windows - Maintain backward compatibility when onTap is nil
1 parent ebec67a commit 9b5d3e6

File tree

5 files changed

+70
-27
lines changed

5 files changed

+70
-27
lines changed

Sources/GeneralNotification/GeneralNotification.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ import SwiftUI
88
let defaultInterval: TimeInterval = 2
99

1010
public enum GeneralNotification {
11-
public static func present(bodyView: some View, interval: TimeInterval = defaultInterval) {
12-
let bodyView = bodyView
11+
public static func present(
12+
bodyView: some View,
13+
interval: TimeInterval = defaultInterval,
14+
onTap: (() -> Void)? = nil
15+
) {
16+
let bodyView =
17+
bodyView
1318
.font(.system(.body, design: .rounded))
1419
.frame(maxWidth: 320)
1520

16-
guard let context = NotificationContext(
17-
bodyView: bodyView
18-
) else {
21+
guard
22+
let context = NotificationContext(
23+
bodyView: bodyView,
24+
onTap: onTap
25+
)
26+
else {
1927
return
2028
}
2129

Sources/GeneralNotification/NotificationContext.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import SwiftUI
1212
struct NotificationContext {
1313
let screen: NSScreen
1414
let bodyView: AnyView
15+
let onTap: (() -> Void)?
1516

16-
init(screen: NSScreen, bodyView: AnyView) {
17+
init(screen: NSScreen, bodyView: AnyView, onTap: (() -> Void)? = nil) {
1718
self.screen = screen
1819
self.bodyView = bodyView
20+
self.onTap = onTap
1921
}
2022

21-
init?(bodyView: AnyView) {
23+
init?(bodyView: AnyView, onTap: (() -> Void)? = nil) {
2224
let mouseLocation = NSEvent.mouseLocation
2325
let screens = NSScreen.screens
2426
let screenWithMouse =
@@ -29,12 +31,13 @@ struct NotificationContext {
2931
}
3032
self.init(
3133
screen: screen,
32-
bodyView: bodyView
34+
bodyView: bodyView,
35+
onTap: onTap
3336
)
3437
}
3538

36-
init?(bodyView: some View) {
37-
self.init(bodyView: AnyView(bodyView))
39+
init?(bodyView: some View, onTap: (() -> Void)? = nil) {
40+
self.init(bodyView: AnyView(bodyView), onTap: onTap)
3841
}
3942

4043
func open(forInterval interval: TimeInterval = 0) {
@@ -43,7 +46,8 @@ struct NotificationContext {
4346

4447
let viewModel = NotificationViewModel(
4548
screen: screen,
46-
bodyView: bodyView
49+
bodyView: bodyView,
50+
onTap: onTap
4751
)
4852
let view = NotificationView(vm: viewModel)
4953
let viewController = NotificationViewController(view)
@@ -55,12 +59,12 @@ struct NotificationContext {
5559
x: screen.frame.origin.x,
5660
y: screen.frame.origin.y + screen.frame.height - viewModel.notificationOpenedSize.height - shadowInset - 24,
5761
width: screen.frame.width,
58-
height: viewModel.notificationOpenedSize.height + shadowInset // for shadow
62+
height: viewModel.notificationOpenedSize.height + shadowInset // for shadow
5963
)
6064
window.window?.setFrameOrigin(topRect.origin)
6165
window.window?.setContentSize(topRect.size)
6266

63-
// window.showWindow(nil)
67+
// window.showWindow(nil)
6468
window.window?.orderFront(nil)
6569

6670
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {

Sources/GeneralNotification/NotificationView.swift

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,38 @@ struct NotificationView: View {
2222
}
2323
}
2424

25+
@ViewBuilder
26+
private var contentView: some View {
27+
vm.bodyView
28+
.frame(maxWidth: vm.notificationOpenedSize.width, maxHeight: vm.notificationOpenedSize.height)
29+
.zIndex(1)
30+
.background(
31+
RoundedRectangle(cornerRadius: 16).stroke(lineWidth: 2).foregroundStyle(
32+
systemColorScheme == .dark ? Color.primary.opacity(0.2) : .white.opacity(0.3))
33+
)
34+
.background(.regularMaterial)
35+
.focusable(false)
36+
.clipShape(RoundedRectangle(cornerRadius: 16))
37+
.shadow(color: .black.opacity(0.2), radius: 4, y: 1)
38+
}
39+
40+
@ViewBuilder
41+
private var notificationContent: some View {
42+
if let onTap = vm.onTap {
43+
Button(action: onTap) {
44+
contentView
45+
}
46+
.buttonStyle(.plain)
47+
} else {
48+
contentView
49+
}
50+
}
51+
2552
var body: some View {
2653
ZStack(alignment: .top) {
2754
Group {
2855
if vm.status == .opened {
29-
vm.bodyView
30-
.frame(maxWidth: vm.notificationOpenedSize.width, maxHeight: vm.notificationOpenedSize.height)
31-
.zIndex(1)
32-
.background(
33-
RoundedRectangle(cornerRadius: 16).stroke(lineWidth: 2).foregroundStyle(
34-
systemColorScheme == .dark ? Color.primary.opacity(0.2) : .white.opacity(0.3))
35-
)
36-
.background(.regularMaterial)
37-
.focusable(false)
38-
.clipShape(RoundedRectangle(cornerRadius: 16))
39-
.shadow(color: .black.opacity(0.2), radius: 4, y: 1)
56+
notificationContent
4057
}
4158
}
4259
.transition(

Sources/GeneralNotification/NotificationViewController.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import AppKit
99
import Cocoa
1010
import SwiftUI
1111

12+
class ClickableHostingView<Content: View>: NSHostingView<Content> {
13+
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
14+
true
15+
}
16+
}
17+
1218
class NotificationViewController: NSHostingController<AnyView> {
1319
init(_ view: AnyView) {
1420
super.init(rootView: view)
@@ -22,4 +28,8 @@ class NotificationViewController: NSHostingController<AnyView> {
2228
convenience init(_ view: some View) {
2329
self.init(AnyView(view))
2430
}
31+
32+
override func loadView() {
33+
view = ClickableHostingView(rootView: rootView)
34+
}
2535
}

Sources/GeneralNotification/NotificationViewModel.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ class NotificationViewModel: NSObject, ObservableObject {
1010

1111
let cornerRadius: CGFloat
1212

13+
let onTap: (() -> Void)?
14+
1315
var referencedWindow: NotificationWindowController? = nil
1416

15-
init(screen: NSScreen, bodyView: AnyView) {
17+
init(screen: NSScreen, bodyView: AnyView, onTap: (() -> Void)? = nil) {
1618
self.bodyView = AnyView(bodyView.padding(8))
19+
self.onTap = onTap
1720

1821
let bodyFittingSize = NSHostingView(rootView: self.bodyView).fittingSize
1922

@@ -27,10 +30,11 @@ class NotificationViewModel: NSObject, ObservableObject {
2730
super.init()
2831
}
2932

30-
convenience init(screen: NSScreen, bodyView: some View) {
33+
convenience init(screen: NSScreen, bodyView: some View, onTap: (() -> Void)? = nil) {
3134
self.init(
3235
screen: screen,
33-
bodyView: AnyView(bodyView)
36+
bodyView: AnyView(bodyView),
37+
onTap: onTap
3438
)
3539
}
3640

0 commit comments

Comments
 (0)