Skip to content

Commit d2a9d7b

Browse files
committed
feat: 푸시 알람을 row를 탭하면 관련 TODO가 뜸
1 parent 288de04 commit d2a9d7b

3 files changed

Lines changed: 127 additions & 51 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// TodoIDItem.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/17/26.
6+
//
7+
8+
import Foundation
9+
10+
struct TodoIDItem: Identifiable, Hashable {
11+
let id: String
12+
}

DevLog/Presentation/ViewModel/PushNotificationViewModel.swift

Lines changed: 92 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ final class PushNotificationViewModel: Store {
2222
var sortOption: SortOption
2323
var timeFilter: TimeFilter
2424
var showUnreadOnly: Bool
25+
var selectedTodoID: TodoIDItem?
2526
}
2627

2728
enum Action {
@@ -38,10 +39,12 @@ final class PushNotificationViewModel: Store {
3839
case setTimeFilter(TimeFilter)
3940
case toggleUnreadOnly
4041
case resetFilters
42+
case tapNotification(PushNotification)
43+
case setSelectedTodoID(TodoIDItem?)
4144
}
4245

4346
enum SideEffect {
44-
case fetch
47+
case fetchNotifications
4548
case delete(PushNotification)
4649
case toggleRead(String)
4750
}
@@ -116,7 +119,7 @@ final class PushNotificationViewModel: Store {
116119
}
117120

118121
@Published private(set) var state: State
119-
private let fetchUseCase: FetchPushNotificationsUseCase
122+
private let fetchNotificationUseCase: FetchPushNotificationsUseCase
120123
private let deleteUseCase: DeletePushNotificationUseCase
121124
private let toggleReadUseCase: TogglePushNotificationReadUseCase
122125
private let userDefaults: UserDefaults
@@ -133,7 +136,7 @@ final class PushNotificationViewModel: Store {
133136
toggleReadUseCase: TogglePushNotificationReadUseCase,
134137
userDefaults: UserDefaults = .standard
135138
) {
136-
self.fetchUseCase = fetchUseCase
139+
self.fetchNotificationUseCase = fetchUseCase
137140
self.deleteUseCase = deleteUseCase
138141
self.toggleReadUseCase = toggleReadUseCase
139142
self.userDefaults = userDefaults
@@ -180,51 +183,15 @@ final class PushNotificationViewModel: Store {
180183
var effects: [SideEffect] = []
181184

182185
switch action {
183-
case .fetchNotifications:
184-
effects = [.fetch]
185-
case .deleteNotification(let item):
186-
guard let index = state.notifications.firstIndex(where: { $0.id == item.id }) else {
187-
break
188-
}
189-
state.pendingTask = (item, index)
190-
state.notifications.remove(at: index)
191-
setToast(&state, isPresented: true, for: .delete)
192-
case .toggleRead(let item):
193-
if let index = state.notifications.firstIndex(where: { $0.id == item.id }) {
194-
state.notifications[index].isRead.toggle()
195-
effects = [.toggleRead(item.todoID)]
196-
}
197-
case .undoDelete:
198-
guard let (item, index) = state.pendingTask else { break }
199-
state.notifications.insert(item, at: index)
200-
state.pendingTask = nil
201-
case .confirmDelete:
202-
guard let (item, _ ) = state.pendingTask else { break }
203-
effects = [.delete(item)]
204-
case .setAlert(let isPresented, let type):
205-
setAlert(&state, isPresented: isPresented, for: type)
206-
case .setToast(let isPresented, let type):
207-
setToast(&state, isPresented: isPresented, for: type)
208-
case .setLoading(let value):
209-
state.isLoading = value
210-
case .setNotifications(let notifications):
211-
state.notifications = notifications
212-
case .toggleSortOption:
213-
state.sortOption = state.sortOption == .latest ? .oldest : .latest
214-
saveSortOption(state.sortOption)
215-
case .setTimeFilter(let filter):
216-
state.timeFilter = filter
217-
saveTimeFilter(filter)
218-
case .toggleUnreadOnly:
219-
state.showUnreadOnly.toggle()
220-
userDefaults.set(state.showUnreadOnly, forKey: DefaultsKey.showUnreadOnly)
221-
case .resetFilters:
222-
state.sortOption = .latest
223-
state.timeFilter = .none
224-
state.showUnreadOnly = false
225-
saveSortOption(.latest)
226-
saveTimeFilter(.none)
227-
userDefaults.set(false, forKey: DefaultsKey.showUnreadOnly)
186+
case .deleteNotification, .toggleRead, .undoDelete, .setAlert, .toggleSortOption,
187+
.setTimeFilter, .toggleUnreadOnly, .resetFilters, .tapNotification:
188+
effects = reduceByUser(action, state: &state)
189+
190+
case .fetchNotifications, .confirmDelete, .setToast, .setSelectedTodoID:
191+
effects = reduceByView(action, state: &state)
192+
193+
case .setLoading, .setNotifications:
194+
effects = reduceByRun(action, state: &state)
228195
}
229196

230197
self.state = state
@@ -233,12 +200,12 @@ final class PushNotificationViewModel: Store {
233200

234201
func run(_ effect: SideEffect) {
235202
switch effect {
236-
case .fetch:
203+
case .fetchNotifications:
237204
Task {
238205
do {
239206
defer { send(.setLoading(false)) }
240207
send(.setLoading(true))
241-
let notifications = try await fetchUseCase.execute()
208+
let notifications = try await fetchNotificationUseCase.execute()
242209
send(.setNotifications(notifications))
243210
} catch {
244211
send(.setAlert(isPresented: true, type: .error))
@@ -264,6 +231,81 @@ final class PushNotificationViewModel: Store {
264231
}
265232
}
266233

234+
// MARK: - Reduce Methods
235+
private extension PushNotificationViewModel {
236+
func reduceByUser(_ action: Action, state: inout State) -> [SideEffect] {
237+
switch action {
238+
case .deleteNotification(let item):
239+
if let index = state.notifications.firstIndex(where: { $0.id == item.id }) {
240+
state.pendingTask = (item, index)
241+
state.notifications.remove(at: index)
242+
setToast(&state, isPresented: true, for: .delete)
243+
}
244+
case .toggleRead(let item):
245+
if let index = state.notifications.firstIndex(where: { $0.id == item.id }) {
246+
state.notifications[index].isRead.toggle()
247+
return [.toggleRead(item.todoID)]
248+
}
249+
case .undoDelete:
250+
guard let (item, index) = state.pendingTask else { return [] }
251+
state.notifications.insert(item, at: index)
252+
state.pendingTask = nil
253+
case .setAlert(let isPresented, let type):
254+
setAlert(&state, isPresented: isPresented, for: type)
255+
case .toggleSortOption:
256+
state.sortOption = state.sortOption == .latest ? .oldest : .latest
257+
saveSortOption(state.sortOption)
258+
case .setTimeFilter(let filter):
259+
state.timeFilter = filter
260+
saveTimeFilter(filter)
261+
case .toggleUnreadOnly:
262+
state.showUnreadOnly.toggle()
263+
userDefaults.set(state.showUnreadOnly, forKey: DefaultsKey.showUnreadOnly)
264+
case .resetFilters:
265+
state.sortOption = .latest
266+
state.timeFilter = .none
267+
state.showUnreadOnly = false
268+
saveSortOption(.latest)
269+
saveTimeFilter(.none)
270+
userDefaults.set(false, forKey: DefaultsKey.showUnreadOnly)
271+
case .tapNotification(let notification):
272+
state.selectedTodoID = TodoIDItem(id: notification.todoID)
273+
default:
274+
break
275+
}
276+
return []
277+
}
278+
279+
func reduceByView(_ action: Action, state: inout State) -> [SideEffect] {
280+
switch action {
281+
case .fetchNotifications:
282+
return [.fetchNotifications]
283+
case .confirmDelete:
284+
guard let (item, _ ) = state.pendingTask else { return [] }
285+
return [.delete(item)]
286+
case .setToast(let isPresented, let type):
287+
setToast(&state, isPresented: isPresented, for: type)
288+
case .setSelectedTodoID(let todoID):
289+
state.selectedTodoID = todoID
290+
default:
291+
break
292+
}
293+
return []
294+
}
295+
296+
func reduceByRun(_ action: Action, state: inout State) -> [SideEffect] {
297+
switch action {
298+
case .setLoading(let value):
299+
state.isLoading = value
300+
case .setNotifications(let notifications):
301+
state.notifications = notifications
302+
default:
303+
break
304+
}
305+
return []
306+
}
307+
}
308+
267309
private extension PushNotificationViewModel {
268310
func setAlert(
269311
_ state: inout State,

DevLog/UI/PushNotification/PushNotificationView.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ struct PushNotificationView: View {
1111
@StateObject private var router = NavigationRouter()
1212
@StateObject var viewModel: PushNotificationViewModel
1313
@Environment(\.colorScheme) private var colorScheme
14+
@Environment(\.diContainer) private var container: DIContainer
1415

1516
var body: some View {
1617
NavigationStack(path: $router.path) {
@@ -26,7 +27,12 @@ struct PushNotificationView: View {
2627
.listRowSeparator(.hidden)
2728
} else {
2829
ForEach(viewModel.displayedNotifications, id: \.id) { notification in
29-
notificationRow(notification)
30+
Button {
31+
viewModel.send(.tapNotification(notification))
32+
} label: {
33+
notificationRow(notification)
34+
}
35+
.buttonStyle(.plain)
3036
}
3137
}
3238
} header: {
@@ -61,6 +67,21 @@ struct PushNotificationView: View {
6167
.multilineTextAlignment(.center)
6268
.lineLimit(3)
6369
}
70+
.sheet(item: Binding(
71+
get: { viewModel.state.selectedTodoID },
72+
set: { viewModel.send(.setSelectedTodoID($0)) }
73+
)) { item in
74+
VStack(spacing: 0) {
75+
Spacer(minLength: 16)
76+
TodoDetailView(viewModel: TodoDetailViewModel(
77+
fetchUseCase: container.resolve(FetchTodoByIDUseCase.self),
78+
upsertUseCase: container.resolve(UpsertTodoUseCase.self),
79+
todoID: item.id
80+
))
81+
}
82+
.background(Color(.secondarySystemBackground))
83+
.presentationDragIndicator(.visible)
84+
}
6485
}
6586
}
6687

@@ -164,6 +185,7 @@ struct PushNotificationView: View {
164185
}
165186
}
166187
.padding(.vertical, 5)
188+
.contentShape(.rect)
167189
.swipeActions(edge: .leading) {
168190
Button {
169191
viewModel.send(.toggleRead(notification))

0 commit comments

Comments
 (0)