Implementing a Custom Swipe‑to‑Delete Interaction in SwiftUI Using Gesture Modifiers
This article demonstrates how to build a custom left‑swipe delete interaction for SwiftUI list cards by using DragGesture, state handling, visual cues, confirmation alerts, hint text, and haptic feedback, offering a more flexible alternative to the built‑in List deletion.
In the previous chapter we introduced the MVVM pattern and a simple delete action triggered by SwiftUI's contextMenu on a long‑press; this method deletes an item by calling a ViewModel function with the item's UUID.
For card‑style lists a more common UX is a left‑to‑right swipe that reveals a delete action. The built‑in EditButton swipe often has limitations, so we implement a custom gesture‑based solution.
SwiftUI provides three main gestures: onTapGesture , LongPressGesture , and DragGesture . We start by attaching a DragGesture to the card view:
// Drag gesture
.gesture(
DragGesture()
.onChanged { value in
// actions while dragging
}
.onEnded { value in
// actions when drag ends
}
)We declare a @GestureState (or a regular @State ) called viewState to store the current translation:
@State var viewState = CGSize.zeroThe card view receives an .offset modifier that only allows movement to the left:
// Allow dragging only from right to left
.offset(x: self.viewState.width < 0 ? self.viewState.width : 0)During the drag we update viewState with the translation and reset it to .zero when the gesture ends:
.gesture(
DragGesture()
.onChanged { value in
self.viewState = value.translation
}
.onEnded { value in
self.viewState = .zero
}
)To decide when a swipe should trigger deletion we define a threshold and a Boolean flag:
@State var valueToBeDeleted: CGFloat = -75
@State var readyToBeDeleted: Bool = falseInside the gesture we compare the current offset with the threshold:
self.readyToBeDeleted = self.viewState.width < self.valueToBeDeleted ? true : falseIf the flag is true we change the card background to red, otherwise keep it white:
.background(self.readyToBeDeleted ? Color(.systemRed) : .white)When the drag finishes we reset readyToBeDeleted to false so the UI returns to its normal state.
The actual removal is performed in the ViewModel. We add a method to fetch an item by its UUID and another to delete it:
// Get item by UUID
func getItemById(itemId: UUID) -> Model? {
return models.first { $0.id == itemId } ?? nil
}
// Delete item
func deleteItem(itemId: UUID) { /* implementation */ }Each CardView receives the ViewModel, the item’s UUID, and a computed item property that calls getItemById :
var viewModel: ViewModel
var itemId: UUID
var item: Model? { viewModel.getItemById(itemId: itemId) }When the swipe passes the threshold we present a confirmation alert before calling deleteItem :
@State var showDeleteAlert: Bool = false
private var deleteAlert: Alert {
Alert(
title: Text(""),
message: Text("确定要删除吗?"),
primaryButton: .destructive(Text("确认")) { /* delete */ },
secondaryButton: .cancel(Text("取消"))
)
}
// Attach alert to the card view
.alert(isPresented: $showDeleteAlert) { deleteAlert }We also add a hint text that becomes visible behind the card when it is swiped left:
HStack {
Spacer()
Text("左滑删除")
.padding()
.foregroundColor(Color(.systemGray))
}To give tactile feedback we create a small utility struct:
import Foundation
import SwiftUI
struct Haptics {
static func hapticSuccess() {
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.success)
}
static func hapticWarning() {
let generator = UINotificationFeedbackGenerator()
generator.notificationOccurred(.warning)
}
}During the swipe we call Haptics.hapticWarning() to signal a potentially destructive action.
By combining gesture handling, state management, visual cues, confirmation alerts, hint text, and haptic feedback we achieve a polished, custom swipe‑to‑delete experience that goes beyond the default List behavior, illustrating how SwiftUI can be extended when built‑in components are insufficient.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.