Skip to content

Commit e3bc85d

Browse files
committed
[FileManager] Delete Swipe
1 parent 3d19f33 commit e3bc85d

File tree

2 files changed

+152
-33
lines changed

2 files changed

+152
-33
lines changed

Flipper/iOS/UI/FileManager/Components/ElementRow.swift

Lines changed: 145 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,50 @@ import Core
33
import SwiftUI
44

55
extension FileManagerView.FileManagerListing {
6-
struct ElementRow: View {
6+
struct ElementRowGrid: View {
77
let element: ExtendedElement
8-
let type: FileManagerSettings.DisplayType
98

10-
let onAction: () -> Void
9+
let onSelect: () -> Void
10+
let onTap: () -> Void
1111

1212
var body: some View {
13-
Group {
14-
switch type {
15-
case .grid:
16-
VStack(alignment: .leading, spacing: 12) {
17-
HStack {
18-
Icon(for: element)
19-
Spacer()
20-
Action(onTap: onAction)
21-
}
22-
Title(for: element)
23-
}
24-
case .list:
25-
HStack(spacing: 12) {
26-
Icon(for: element)
27-
Title(for: element)
28-
Spacer()
29-
Action(onTap: onAction)
30-
}
13+
VStack(alignment: .leading, spacing: 12) {
14+
HStack {
15+
Icon(for: element)
16+
Spacer()
17+
Action(onTap: onSelect)
3118
}
19+
Title(for: element)
3220
}
3321
.padding(12)
3422
.background(Color.groupedBackground)
3523
.cornerRadius(12)
24+
.onTapGesture { onTap() }
25+
}
26+
}
27+
28+
struct ElementRowList: View {
29+
let element: ExtendedElement
30+
31+
let onSelect: () -> Void
32+
let onDelete: () -> Void
33+
let onTap: () -> Void
34+
35+
var body: some View {
36+
HStack(spacing: 12) {
37+
Icon(for: element)
38+
Title(for: element)
39+
Spacer()
40+
Action(onTap: onSelect)
41+
}
42+
.padding(12)
43+
.background(Color.groupedBackground)
44+
.modifier(SwipeToDeleteModifier(onDelete: onDelete, onTap: onTap))
3645
}
3746
}
3847
}
3948

40-
fileprivate extension FileManagerView.FileManagerListing.ElementRow {
49+
fileprivate extension FileManagerView.FileManagerListing {
4150
struct Icon: View {
4251
let element: ExtendedElement
4352

@@ -46,10 +55,11 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow {
4655
case .directory:
4756
return .init("Folder")
4857
case .file:
49-
if let item = try? ArchiveItem.Kind(filename: element.name) {
58+
do {
59+
let item = try ArchiveItem.Kind(filename: element.name)
5060
return item.icon
51-
} else {
52-
return .init("File")
61+
} catch {
62+
return Image("File")
5363
}
5464
}
5565
}
@@ -107,3 +117,113 @@ fileprivate extension FileManagerView.FileManagerListing.ElementRow {
107117
}
108118
}
109119
}
120+
121+
fileprivate extension FileManagerView.FileManagerListing {
122+
struct SwipeToDeleteModifier: ViewModifier {
123+
@State private var offset: CGFloat = 0
124+
@GestureState private var isDragging: Bool = false
125+
126+
let onDelete: () -> Void
127+
let onTap: () -> Void
128+
129+
private var iconSize: Double { 24 }
130+
private var iconPadding: Double { 16 }
131+
132+
private var deleteThreshold: CGFloat { -(iconSize + iconPadding * 2) }
133+
private var fullDeleteThreshold: CGFloat { -120 }
134+
135+
private var delay: Double { 0.5 }
136+
private var animation: Animation { .easeOut(duration: delay) }
137+
138+
private var maxRadius: CGFloat { 12 }
139+
private var radius: CGFloat {
140+
let progress = min(1, abs(offset) / abs(deleteThreshold))
141+
return maxRadius * (1 - progress)
142+
}
143+
144+
func body(content: Content) -> some View {
145+
ZStack {
146+
Rectangle()
147+
.foregroundColor(.red.opacity(0.1))
148+
.cornerRadius(12)
149+
.overlay(
150+
Image("Delete")
151+
.resizable()
152+
.renderingMode(.template)
153+
.foregroundColor(.red)
154+
.frame(width: iconSize, height: iconSize)
155+
.padding(16)
156+
.contentShape(Rectangle())
157+
.onTapGesture {
158+
withAnimation(animation) {
159+
offset = 0
160+
}
161+
onDelete()
162+
},
163+
alignment: .trailing
164+
)
165+
166+
content
167+
.clipShape(
168+
.rect(
169+
topLeadingRadius: maxRadius,
170+
bottomLeadingRadius: maxRadius,
171+
bottomTrailingRadius: radius,
172+
topTrailingRadius: radius
173+
)
174+
)
175+
.offset(x: offset)
176+
.simultaneousGesture(
177+
DragGesture(
178+
minimumDistance: 50,
179+
coordinateSpace: .local
180+
)
181+
.updating($isDragging) { _, state, _ in
182+
state = true
183+
}
184+
.onChanged { value in
185+
let translation = value.translation.width
186+
if translation <= 0 {
187+
offset = translation
188+
}
189+
}
190+
.onEnded { value in
191+
let translation = value.translation.width
192+
193+
if translation <= fullDeleteThreshold {
194+
withAnimation(animation) {
195+
offset = -UIScreen.main.bounds.width
196+
}
197+
198+
Task { @MainActor in
199+
try await Task.sleep(seconds: delay)
200+
onDelete()
201+
202+
withAnimation(animation) {
203+
offset = 0
204+
}
205+
}
206+
} else if translation <= deleteThreshold {
207+
withAnimation(animation) {
208+
offset = deleteThreshold
209+
}
210+
} else {
211+
withAnimation(animation) {
212+
offset = 0
213+
}
214+
}
215+
}
216+
)
217+
.onTapGesture {
218+
if offset != 0 {
219+
withAnimation(animation) {
220+
offset = 0
221+
}
222+
} else {
223+
onTap()
224+
}
225+
}
226+
}
227+
}
228+
}
229+
}

Flipper/iOS/UI/FileManager/Components/FileManagerElements.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,22 @@ extension FileManagerView.FileManagerListing {
1818
case .list:
1919
LazyVStack(spacing: 12) {
2020
ForEach(elements) { element in
21-
ElementRow(
21+
ElementRowList(
2222
element: element,
23-
type: displayType,
24-
onAction: { onSelect(element) }
23+
onSelect: { onSelect(element) },
24+
onDelete: { onDelete(element) },
25+
onTap: { onTap(element) }
2526
)
26-
.onTapGesture { onTap(element) }
2727
}
2828
}
2929
case .grid:
3030
LazyVGrid(columns: columns, spacing: 12) {
3131
ForEach(elements) { element in
32-
ElementRow(
32+
ElementRowGrid(
3333
element: element,
34-
type: displayType,
35-
onAction: { onSelect(element) }
34+
onSelect: { onSelect(element) },
35+
onTap: { onTap(element) }
3636
)
37-
.onTapGesture { onTap(element) }
3837
}
3938
}
4039
}

0 commit comments

Comments
 (0)