From 35d38aa154534d540d198a7417155b6ab5d0423e Mon Sep 17 00:00:00 2001 From: opficdev Date: Thu, 19 Feb 2026 23:25:58 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20toast=20=EA=B0=80=20=EB=9C=AC=20?= =?UTF-8?q?=EC=9D=B4=ED=9B=84=20=EB=B7=B0=EA=B0=80=20=EC=A0=84=ED=99=98?= =?UTF-8?q?=EB=90=9C=20=ED=9B=84=20=EB=8B=A4=EC=8B=9C=20=EC=9B=90=EB=B3=B5?= =?UTF-8?q?=EB=90=A0=20=EB=95=8C=20duration=EC=9D=B4=20=EB=82=A8=EC=95=84?= =?UTF-8?q?=20=EC=9E=88=EC=9C=BC=EB=A9=B4=20=EB=9C=A8=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DevLog/UI/Common/Componeent/Toast.swift | 54 ++++++++++++++++++------- 1 file changed, 40 insertions(+), 14 deletions(-) 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 } } } From 01c049d7f1401cef47760554f4cafb740c6e8efc Mon Sep 17 00:00:00 2001 From: opficdev Date: Fri, 20 Feb 2026 09:34:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=ED=86=A0=EC=8A=A4=ED=8A=B8=20UI=20?= =?UTF-8?q?=EB=A1=A4=EB=B0=B1=EC=97=90=20=EC=9D=98=ED=95=9C=20pendingTask?= =?UTF-8?q?=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModel/PushNotificationViewModel.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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