diff --git a/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift b/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift index 94cc4d6..7479c55 100644 --- a/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift +++ b/DevLog/Presentation/ViewModel/PushNotificationViewModel.swift @@ -216,10 +216,11 @@ private extension PushNotificationViewModel { state.nextCursor = nil return [.fetchNotifications(state.query, cursor: nil)] case .loadNextPage: - guard state.hasMore, !state.isLoading else { return [] } + guard state.hasMore, !state.isLoading, state.pendingTask == nil else { return [] } return [.fetchNotifications(state.query, cursor: state.nextCursor)] case .confirmDelete: - guard let (item, _ ) = state.pendingTask else { return [] } + guard let (item, _) = state.pendingTask else { return [] } + state.pendingTask = nil return [.delete(item)] case .setToast(let isPresented, let type): setToast(&state, isPresented: isPresented, for: type) @@ -241,7 +242,13 @@ private extension PushNotificationViewModel { state.notifications = [] state.nextCursor = nil case .appendNotifications(let notifications, let nextCursor): - state.notifications.append(contentsOf: notifications) + let filteredNotifications: [PushNotification] + if let (pendingItem, _) = state.pendingTask { + filteredNotifications = notifications.filter { $0.id != pendingItem.id } + } else { + filteredNotifications = notifications + } + state.notifications.append(contentsOf: filteredNotifications) state.nextCursor = nextCursor default: break diff --git a/DevLog/UI/Common/Componeent/Toast.swift b/DevLog/UI/Common/Componeent/Toast.swift index 8f7cf4d..8153fae 100644 --- a/DevLog/UI/Common/Componeent/Toast.swift +++ b/DevLog/UI/Common/Componeent/Toast.swift @@ -41,6 +41,7 @@ private struct ToastOverlayView: View { @State private var opacityValue: Double = 0 @State private var dismissWorkItem: DispatchWorkItem? @State private var isTapped: Bool = false + @State private var isScheduled: Bool = false var body: some View { if isPresented { @@ -50,19 +51,18 @@ private struct ToastOverlayView: View { ) .offset(y: yOffset) .opacity(opacityValue) + .onChange(of: isPresented) { newValue in + if newValue { + resetForNewPresentation() + presentAnimated() + scheduleDismissIfNeeded() + } else { + cleanupPresentation() + } + } .onAppear { presentAnimated() - scheduleDismiss() - } - .onDisappear { - dismissWorkItem?.cancel() - dismissWorkItem = nil - isPresented = false - - // 토스트를 탭하지 않고 자동으로 사라진 경우에만 onDismiss 호출 - if !isTapped { - onDismiss?() - } + scheduleDismissIfNeeded() } .onTapGesture { isTapped = true @@ -74,8 +74,7 @@ private struct ToastOverlayView: View { } private func presentAnimated() { - dismissWorkItem?.cancel() - dismissWorkItem = nil + guard opacityValue == 0 else { return } withAnimation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0.0)) { yOffset = -100 @@ -83,7 +82,28 @@ private struct ToastOverlayView: View { } } - private func scheduleDismiss() { + private func resetForNewPresentation() { + dismissWorkItem?.cancel() + dismissWorkItem = nil + isScheduled = false + isTapped = false + yOffset = 0 + opacityValue = 0 + } + + private func cleanupPresentation() { + dismissWorkItem?.cancel() + dismissWorkItem = nil + isScheduled = false + isTapped = false + yOffset = 0 + opacityValue = 0 + } + + private func scheduleDismissIfNeeded() { + guard !isScheduled else { return } + isScheduled = true + let workItem = DispatchWorkItem { dismissAnimated() } @@ -102,6 +122,12 @@ private struct ToastOverlayView: View { DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { isPresented = false + isScheduled = false + + if !isTapped { + onDismiss?() + } + isTapped = false } } }