@@ -18,73 +18,23 @@ struct PushNotificationListView: View {
1818
1919 var body : some View {
2020 NavigationStack ( path: $router. path) {
21- List {
22- Group {
23- if viewModel. state. notifications. isEmpty {
24- HStack {
25- Spacer ( )
26- Text ( " 받은 알림이 없습니다. " )
27- . foregroundStyle ( Color . gray)
28- Spacer ( )
29- }
30- . listRowSeparator ( . hidden)
31- } else {
32- let notifications = viewModel. state. notifications
33- ForEach ( Array ( zip ( notifications. indices, notifications) ) , id: \. 1 . id) { idx, notification in
34- Button {
35- viewModel. send ( . tapNotification( notification) )
36- } label: {
37- notificationRow ( notification)
38- . padding ( . vertical, 8 )
39- }
40- . buttonStyle ( . plain)
41- . onAppear {
42- let lastID = viewModel. state. notifications. last? . id
43- if notification. id == lastID, viewModel. state. hasMore {
44- viewModel. send ( . loadNextPage)
45- }
46- }
47- . listRowInsets ( EdgeInsets ( top: 0 , leading: 16 , bottom: 0 , trailing: 16 ) )
48- . overlay ( alignment: . top) {
49- if #available( iOS 26 . 0 , * ) {
50- if idx == 0 {
51- Divider ( )
52- . padding ( . horizontal, - 16 )
53- }
54- }
55- }
56- }
57- }
58- }
59- . listSectionSeparator ( . hidden, edges: . top)
60- . listRowBackground ( Color . clear)
61- }
21+ notificationList
6222 . listStyle ( . plain)
63- . background ( NavigationBarConfigurator ( . secondarySystemBackground) )
23+ . background ( NavigationBarConfigurator ( . secondarySystemBackground, alwaysVisible : true ) )
6424 . onScrollOffsetChange { offset in
6525 guard isScrollTrackingEnabled else { return }
6626 headerOffset = max ( 0 , - offset)
6727 }
68- . safeAreaInset ( edge: . top) {
69- VStack ( spacing: 4 ) {
70- headerView
71- . clipped ( )
72- if #unavailable( iOS 26 ) {
73- Divider ( )
74- . padding ( . horizontal, - 16 )
75- }
76- }
77- . background {
78- if #available( iOS 26 . 0 , * ) {
79- Color . clear
80- } else {
81- Color ( . secondarySystemBackground)
82- }
28+ . safeAreaInset ( edge: . top) { safeAreaHeader }
29+ . background ( Color ( . secondarySystemBackground) )
30+ . onAppear {
31+ viewModel. send ( . fetchNotifications)
32+ headerOffset = 0
33+ isScrollTrackingEnabled = false
34+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.3 ) {
35+ isScrollTrackingEnabled = true
8336 }
84- . offset ( y: headerOffset)
8537 }
86- . background ( Color ( . secondarySystemBackground) )
87- . onAppear { viewModel. send ( . fetchNotifications) }
8838 . refreshable { viewModel. send ( . fetchNotifications) }
8939 . navigationTitle ( " 받은 푸시 알람 " )
9040 . alert (
@@ -138,77 +88,143 @@ struct PushNotificationListView: View {
13888 }
13989 }
14090
141- private var headerView : some View {
142- ScrollView ( . horizontal) {
143- HStack ( spacing: 8 ) {
144- if 0 < viewModel. appliedFilterCount {
145- Menu {
146- Text ( " \( viewModel. appliedFilterCount) 개 필터가 적용됨 " )
147- Button ( role: . destructive) {
148- viewModel. send ( . resetFilters)
91+ private var notificationList : some View {
92+ List {
93+ Group {
94+ if viewModel. state. notifications. isEmpty {
95+ HStack {
96+ Spacer ( )
97+ Text ( " 받은 알림이 없습니다. " )
98+ . foregroundStyle ( Color . gray)
99+ Spacer ( )
100+ }
101+ . listRowSeparator ( . hidden)
102+ } else {
103+ let notifications = viewModel. state. notifications
104+ ForEach ( Array ( zip ( notifications. indices, notifications) ) , id: \. 1 . id) { idx, notification in
105+ Button {
106+ viewModel. send ( . tapNotification( notification) )
149107 } label: {
150- Text ( " 모든 필터 지우기 " )
108+ notificationRow ( notification)
109+ . padding ( . vertical, 8 )
151110 }
152- } label: {
153- HStack ( spacing: 6 ) {
154- Image ( systemName: " line.3.horizontal.decrease " )
155- filterBadge
111+ . buttonStyle ( . plain)
112+ . onAppear {
113+ let lastID = viewModel. state. notifications. last? . id
114+ if notification. id == lastID, viewModel. state. hasMore {
115+ viewModel. send ( . loadNextPage)
116+ }
117+ }
118+ . listRowInsets ( EdgeInsets ( top: 0 , leading: 16 , bottom: 0 , trailing: 16 ) )
119+ . overlay ( alignment: . top) {
120+ if #available( iOS 26 . 0 , * ) {
121+ if idx == 0 {
122+ Divider ( )
123+ . padding ( . horizontal, - 16 )
124+ }
125+ }
156126 }
157- . adaptiveButtonStyle ( )
158127 }
159128 }
129+ }
130+ . listSectionSeparator ( . hidden, edges: . top)
131+ . listRowBackground ( Color . clear)
132+ }
133+ }
160134
161- Button {
162- viewModel. send ( . toggleSortOption)
163- } label: {
164- let condition = viewModel. state. query. sortOrder == . oldest
165- Text ( " 정렬: \( viewModel. state. query. sortOrder. title) " )
166- . foregroundStyle ( condition ? . white : Color ( . label) )
167- . adaptiveButtonStyle ( color: condition ? . blue : . clear)
168- }
135+ private var safeAreaHeader : some View {
136+ VStack ( spacing: 4 ) {
137+ headerView
138+ . clipped ( )
139+ if #unavailable( iOS 26 ) {
140+ Divider ( )
141+ . padding ( . horizontal, - 16 )
142+ }
143+ }
144+ . background {
145+ if #available( iOS 26 . 0 , * ) {
146+ Color . clear
147+ } else {
148+ Color ( . secondarySystemBackground)
149+ }
150+ }
151+ . offset ( y: headerOffset)
152+ }
169153
154+ private var headerView : some View {
155+ Group {
156+ if #available( iOS 18 , * ) {
157+ ScrollView ( . horizontal) { headerContent }
158+ . scrollIndicators ( . never)
159+ . scrollDisabled ( !isScrollTrackingEnabled)
160+ . contentMargins ( . leading, 16 , for: . scrollContent)
161+ } else {
162+ headerContent
163+ . padding ( . leading, 16 )
164+ . frame ( maxWidth: . infinity, alignment: . leading)
165+ }
166+ }
167+ }
168+
169+ private var headerContent : some View {
170+ HStack ( spacing: 8 ) {
171+ if 0 < viewModel. appliedFilterCount {
170172 Menu {
171- Picker ( selection: Binding (
172- get: { viewModel. state. query. timeFilter } ,
173- set: { viewModel. send ( . setTimeFilter( $0) ) }
174- ) ) {
175- ForEach ( PushNotificationQuery . TimeFilter. availableOptions, id: \. self) { option in
176- Text ( option. title) . tag ( option)
177- }
173+ Text ( " \( viewModel. appliedFilterCount) 개 필터가 적용됨 " )
174+ Button ( role: . destructive) {
175+ viewModel. send ( . resetFilters)
178176 } label: {
179- Text ( " 기간 " )
177+ Text ( " 모든 필터 지우기 " )
180178 }
181179 } label: {
182- let condition = viewModel. state. query. timeFilter == . none
183- HStack {
184- Text ( " 기간 " )
185- Image ( systemName: " chevron.down " )
180+ HStack ( spacing: 6 ) {
181+ Image ( systemName: " line.3.horizontal.decrease " )
182+ filterBadge
186183 }
187- . foregroundStyle ( condition ? Color ( . label) : . white)
188- . adaptiveButtonStyle ( color: condition ? . clear : . blue)
184+ . adaptiveButtonStyle ( )
189185 }
186+ }
190187
191- Button {
192- viewModel. send ( . toggleUnreadOnly)
188+ Button {
189+ viewModel. send ( . toggleSortOption)
190+ } label: {
191+ let condition = viewModel. state. query. sortOrder == . oldest
192+ Text ( " 정렬: \( viewModel. state. query. sortOrder. title) " )
193+ . foregroundStyle ( condition ? . white : Color ( . label) )
194+ . adaptiveButtonStyle ( color: condition ? . blue : . clear)
195+ }
196+
197+ Menu {
198+ Picker ( selection: Binding (
199+ get: { viewModel. state. query. timeFilter } ,
200+ set: { viewModel. send ( . setTimeFilter( $0) ) }
201+ ) ) {
202+ ForEach ( PushNotificationQuery . TimeFilter. availableOptions, id: \. self) { option in
203+ Text ( option. title) . tag ( option)
204+ }
193205 } label: {
194- let condition = viewModel. state. query. unreadOnly
195- Text ( " 읽지 않음 " )
196- . foregroundStyle ( condition ? . white : Color ( . label) )
197- . adaptiveButtonStyle ( color: condition ? . blue : . clear)
206+ Text ( " 기간 " )
198207 }
208+ } label: {
209+ let condition = viewModel. state. query. timeFilter == . none
210+ HStack {
211+ Text ( " 기간 " )
212+ Image ( systemName: " chevron.down " )
213+ }
214+ . foregroundStyle ( condition ? Color ( . label) : . white)
215+ . adaptiveButtonStyle ( color: condition ? . clear : . blue)
199216 }
200- . frame ( height: 36 )
201- }
202- . scrollIndicators ( . never)
203- . scrollDisabled ( !isScrollTrackingEnabled)
204- . contentMargins ( . leading, 16 , for: . scrollContent)
205- . onAppear {
206- headerOffset = 0
207- isScrollTrackingEnabled = false
208- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.3 ) {
209- isScrollTrackingEnabled = true
217+
218+ Button {
219+ viewModel. send ( . toggleUnreadOnly)
220+ } label: {
221+ let condition = viewModel. state. query. unreadOnly
222+ Text ( " 읽지 않음 " )
223+ . foregroundStyle ( condition ? . white : Color ( . label) )
224+ . adaptiveButtonStyle ( color: condition ? . blue : . clear)
210225 }
211226 }
227+ . frame ( height: 36 )
212228 }
213229
214230 private var filterBadge : some View {
0 commit comments