1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
import SwiftUI
struct SwipeCircleAction {
let label: String
let systemImage: String
let color: Color
let handler: () -> Void
}
struct TicketCircleSwipeRow<Content: View>: View {
let content: Content
let leadingAction: SwipeCircleAction?
let trailingAction: SwipeCircleAction?
@State private var offset: CGFloat = 0
private let circleSize: CGFloat = 60
private let revealDistance: CGFloat = 88
private let triggerDistance: CGFloat = 140
init(
leadingAction: SwipeCircleAction? = nil,
trailingAction: SwipeCircleAction? = nil,
@ViewBuilder content: () -> Content
) {
self.leadingAction = leadingAction
self.trailingAction = trailingAction
self.content = content()
}
var body: some View {
ZStack(alignment: .center) {
if leadingAction != nil {
HStack {
leadingCircle
.padding(.leading, 16)
Spacer()
}
}
if trailingAction != nil {
HStack {
Spacer()
trailingCircle
.padding(.trailing, 16)
}
}
content
.offset(x: clampedOffset)
.gesture(dragGesture)
}
.clipped()
}
@ViewBuilder
private var leadingCircle: some View {
if let action = leadingAction {
let progress = min(max(offset / revealDistance, 0), 1)
circleButton(action: action, progress: progress)
}
}
@ViewBuilder
private var trailingCircle: some View {
if let action = trailingAction {
let progress = min(max(-offset / revealDistance, 0), 1)
circleButton(action: action, progress: progress)
}
}
@ViewBuilder
private func circleButton(action: SwipeCircleAction, progress: CGFloat) -> some View {
Button {
fire(action)
} label: {
VStack(spacing: 5) {
ZStack {
Circle()
.fill(action.color)
.frame(width: circleSize, height: circleSize)
Image(systemName: action.systemImage)
.font(.system(size: 22, weight: .medium))
.foregroundStyle(.white)
}
Text(action.label)
.font(.caption2.weight(.medium))
.foregroundStyle(.white)
}
}
.buttonStyle(.plain)
.opacity(progress)
.scaleEffect(0.7 + 0.3 * progress, anchor: .center)
.animation(.spring(duration: 0.2), value: progress)
}
private var clampedOffset: CGFloat {
if offset > 0 {
return leadingAction != nil ? min(offset, revealDistance + 16) : 0
} else {
return trailingAction != nil ? max(offset, -(revealDistance + 16)) : 0
}
}
private var dragGesture: some Gesture {
DragGesture(minimumDistance: 12, coordinateSpace: .local)
.onChanged { value in
let dx = value.translation.width
if dx > 0, leadingAction != nil {
offset = dx
} else if dx < 0, trailingAction != nil {
offset = dx
}
}
.onEnded { value in
let dx = value.translation.width
if dx > triggerDistance, let action = leadingAction {
fire(action)
} else if dx < -triggerDistance, let action = trailingAction {
fire(action)
} else {
snapBack()
}
}
}
private func fire(_ action: SwipeCircleAction) {
snapBack()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) {
action.handler()
}
}
private func snapBack() {
withAnimation(.spring(duration: 0.35, bounce: 0.2)) {
offset = 0
}
}
}
|