@@ -19,67 +19,100 @@ struct NotificationOverlayView: View {
1919 // Fixed width for notifications
2020 private let notificationWidth : CGFloat = 320 // 300 + 10 padding on each side
2121
22+ @State private var hasOverflow : Bool = false
23+ @State private var contentHeight : CGFloat = 0.0
24+
25+ private func updateOverflow( contentHeight: CGFloat , containerHeight: CGFloat ) {
26+ if !hasOverflow && contentHeight > containerHeight {
27+ hasOverflow = true
28+ } else if hasOverflow && contentHeight <= containerHeight {
29+ hasOverflow = false
30+ }
31+ }
32+
2233 var notifications : some View {
2334 VStack ( spacing: 8 ) {
24- ForEach ( notificationManager. activeNotifications, id: \. id) { notification in
25- if controlActiveState == . active || controlActiveState == . key {
26- NotificationBannerView (
27- notification: notification,
28- onDismiss: {
29- notificationManager. dismissNotification ( notification)
30- } ,
31- onAction: {
32- notification. action ( )
33- notificationManager. dismissNotification ( notification)
34- // Only hide if manually shown
35- if notificationManager. isManuallyShown {
36- notificationManager. toggleNotificationsVisibility ( )
37- }
35+ ForEach (
36+ notificationManager. activeNotifications. filter {
37+ notificationManager. isNotificationVisible ( $0)
38+ } ,
39+ id: \. id
40+ ) { notification in
41+ NotificationBannerView (
42+ notification: notification,
43+ onDismiss: {
44+ notificationManager. dismissNotification ( notification)
45+ } ,
46+ onAction: {
47+ notification. action ( )
48+ notificationManager. dismissNotification ( notification)
49+ // Only hide if manually shown
50+ if notificationManager. isManuallyShown {
51+ notificationManager. toggleNotificationsVisibility ( )
3852 }
39- )
40- . transition ( . asymmetric(
41- insertion: . move( edge: . trailing) ,
42- removal: . opacity
43- ) )
44- }
53+ }
54+ )
4555 }
4656 }
4757 . padding ( 10 )
4858 }
4959
50- var body : some View {
51- ViewThatFits ( in: . vertical) {
52- notifications
53- . border ( . red)
54- GeometryReader { geometry in
55- HStack {
56- Spacer ( ) // Push content to trailing edge
57- ScrollViewReader { proxy in
58- ScrollView ( . vertical, showsIndicators: false ) {
59- VStack ( spacing: 0 ) {
60- // Invisible anchor view at the top to scroll back to when closed
61- Color . clear. frame ( height: 0 ) . id ( topID)
62- notifications
63- }
64- . padding ( . bottom, 30 ) // Account for the status bar
60+ var notificationsWithScrollView : some View {
61+ GeometryReader { geometry in
62+ HStack {
63+ Spacer ( ) // Push content to trailing edge
64+ ScrollViewReader { proxy in
65+ ScrollView ( . vertical, showsIndicators: false ) {
66+ VStack ( alignment: . trailing, spacing: 0 ) {
67+ // Invisible anchor view at the top to scroll back to when closed
68+ Color . clear. frame ( height: 0 ) . id ( topID)
69+ notifications
6570 }
66- . frame ( width: notificationWidth)
67- . frame ( maxHeight: geometry. size. height)
68- . scrollDisabled ( !notificationManager. isManuallyShown)
69- . onChange ( of: notificationManager. isManuallyShown) { isShown in
70- if !isShown {
71- // Delay scroll animation until after notifications are hidden
72- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.3 ) {
73- withAnimation ( . easeOut( duration: 0.3 ) ) {
74- proxy. scrollTo ( topID, anchor: . top)
75- }
71+ . background (
72+ GeometryReader { proxy in
73+ Color . clear. onChange ( of: proxy. size. height) { newValue in
74+ contentHeight = newValue
75+ updateOverflow ( contentHeight: newValue, containerHeight: geometry. size. height)
76+ }
77+ }
78+ )
79+ }
80+ . frame ( maxWidth: notificationWidth, alignment: . trailing)
81+ . frame ( height: min ( geometry. size. height, contentHeight) )
82+ . scrollDisabled ( !hasOverflow)
83+ . onChange ( of: geometry. size. height) { newValue in
84+ updateOverflow ( contentHeight: contentHeight, containerHeight: newValue)
85+ }
86+ . onChange ( of: notificationManager. isManuallyShown) { isShown in
87+ if !isShown {
88+ // Delay scroll animation until after notifications are hidden
89+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.3 ) {
90+ withAnimation ( . easeOut( duration: 0.3 ) ) {
91+ proxy. scrollTo ( topID, anchor: . top)
7692 }
7793 }
7894 }
7995 }
96+ . allowsHitTesting (
97+ notificationManager. activeNotifications
98+ . contains { notificationManager. isNotificationVisible ( $0) }
99+ )
80100 }
81- . animation ( . easeInOut( duration: 0.3 ) , value: notificationManager. activeNotifications)
82101 }
83102 }
84103 }
104+
105+ var body : some View {
106+ Group {
107+ if #available( macOS 14 . 0 , * ) {
108+ notificationsWithScrollView
109+ . scrollClipDisabled ( true )
110+ } else {
111+ notificationsWithScrollView
112+ }
113+ }
114+ . opacity ( controlActiveState == . active || controlActiveState == . key ? 1 : 0 )
115+ . offset ( x: controlActiveState == . active || controlActiveState == . key ? 0 : 350 )
116+ . animation ( . easeInOut( duration: 0.2 ) , value: controlActiveState)
117+ }
85118}
0 commit comments