Skip to content

Commit f6f9914

Browse files
committed
feat: 토스트 구현
1 parent 92a54a8 commit f6f9914

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// Toast.swift
3+
// DevLog
4+
//
5+
// Created by 최윤진 on 2/10/26.
6+
//
7+
8+
import SwiftUI
9+
10+
extension View {
11+
func toast(
12+
isPresented: Binding<Bool>,
13+
duration: TimeInterval = 2,
14+
message: String,
15+
action: (() -> Void)? = nil
16+
) -> some View {
17+
self
18+
.frame(maxWidth: .infinity, maxHeight: .infinity)
19+
.overlay(alignment: .bottom) {
20+
ToastOverlayView(
21+
isPresented: isPresented,
22+
duration: duration,
23+
message: message,
24+
action: action
25+
)
26+
.foregroundStyle(action == nil ? Color(.label) : .blue)
27+
.padding(.horizontal, 12)
28+
}
29+
}
30+
}
31+
32+
private struct ToastOverlayView: View {
33+
@Binding var isPresented: Bool
34+
let duration: TimeInterval
35+
let message: String
36+
let action: (() -> Void)?
37+
38+
@State private var yOffset: CGFloat = 0
39+
@State private var opacityValue: Double = 0
40+
@State private var dismissWorkItem: DispatchWorkItem?
41+
42+
var body: some View {
43+
if isPresented {
44+
ToastCardView(message)
45+
.offset(y: yOffset)
46+
.opacity(opacityValue)
47+
.onAppear {
48+
presentAnimated()
49+
scheduleDismiss()
50+
}
51+
.onDisappear {
52+
dismissWorkItem?.cancel()
53+
dismissWorkItem = nil
54+
isPresented = false
55+
}
56+
.onTapGesture {
57+
dismissAnimated()
58+
action?()
59+
}
60+
.transition(.identity)
61+
}
62+
}
63+
64+
private func presentAnimated() {
65+
dismissWorkItem?.cancel()
66+
dismissWorkItem = nil
67+
68+
withAnimation(.spring(response: 0.5, dampingFraction: 1, blendDuration: 0.0)) {
69+
yOffset = -100
70+
opacityValue = 1
71+
}
72+
}
73+
74+
private func scheduleDismiss() {
75+
let workItem = DispatchWorkItem {
76+
dismissAnimated()
77+
}
78+
dismissWorkItem = workItem
79+
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: workItem)
80+
}
81+
82+
private func dismissAnimated() {
83+
dismissWorkItem?.cancel()
84+
dismissWorkItem = nil
85+
86+
withAnimation(.easeInOut(duration: 0.2)) {
87+
yOffset = 0
88+
opacityValue = 0
89+
}
90+
91+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
92+
isPresented = false
93+
}
94+
}
95+
}
96+
97+
private struct ToastCardView: View {
98+
let message: String
99+
100+
init(_ message: String) {
101+
self.message = message
102+
}
103+
104+
var body: some View {
105+
Text(message)
106+
.font(.caption)
107+
.multilineTextAlignment(.center)
108+
.lineLimit(3)
109+
.padding(.vertical, 12)
110+
.padding(.horizontal, 14)
111+
.background {
112+
if #available(iOS 26.0, *) {
113+
RoundedRectangle(cornerRadius: 16, style: .continuous)
114+
.glassEffect()
115+
} else {
116+
RoundedRectangle(cornerRadius: 16, style: .continuous)
117+
.fill(.ultraThinMaterial)
118+
}
119+
}
120+
.overlay {
121+
if #unavailable(iOS 26.0) {
122+
RoundedRectangle(cornerRadius: 16, style: .continuous)
123+
.strokeBorder(Color(.systemGray4).opacity(0.2), lineWidth: 1)
124+
}
125+
}
126+
.shadow(color: Color(.systemGray2).opacity(0.4), radius: 18, x: 0, y: 10)
127+
}
128+
}

0 commit comments

Comments
 (0)