How to implement swipe to delete in SwiftUI
Attach .swipeActions(edge: .trailing) to any row inside a List, then place a Button with role: .destructive inside it. SwiftUI handles the red background, animation, and accessibility automatically.
List {
ForEach(items) { item in
Text(item.name)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
delete(item)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
Full implementation
The example below creates a simple grocery list. Each row exposes a trailing destructive swipe action that removes the item from the @State array. Because swipeActions are declared per-row, different rows can expose completely different actions — useful when some items are deletable and others are not.
import SwiftUI
struct GroceryItem: Identifiable {
let id = UUID()
var name: String
var emoji: String
}
struct GroceryListView: View {
@State private var items: [GroceryItem] = [
GroceryItem(name: "Avocados", emoji: "🥑"),
GroceryItem(name: "Sourdough", emoji: "🍞"),
GroceryItem(name: "Oat Milk", emoji: "🥛"),
GroceryItem(name: "Blueberries", emoji: "🫐"),
GroceryItem(name: "Dark Chocolate", emoji: "🍫"),
]
var body: some View {
NavigationStack {
List {
ForEach(items) { item in
Label(item.name, systemImage: "")
// Replace Label's icon with the emoji
.overlay(alignment: .leading) {
Text(item.emoji)
.font(.title3)
}
.padding(.leading, 28)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
deleteItem(item)
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.navigationTitle("Groceries")
.animation(.default, value: items)
}
}
private func deleteItem(_ item: GroceryItem) {
items.removeAll { $0.id == item.id }
}
}
#Preview {
GroceryListView()
}
How it works
-
ForEach + Identifiable — The list iterates over
GroceryItemvalues that conform toIdentifiable. SwiftUI uses the stableidto animate removals correctly without index bookkeeping. -
.swipeActions(edge: .trailing, allowsFullSwipe: true) — This modifier attaches the action slot to the trailing edge. Setting
allowsFullSwipe: true(the default) lets a full swipe immediately trigger the first button — the standard Mail.app behavior users expect. -
Button(role: .destructive) — Passing
role: .destructiveautomatically tints the swipe slot red and tells VoiceOver the action is irreversible. You get correct semantics with zero styling code. -
deleteItem(_:) — The helper calls
removeAllusing the item'sidrather than an index, so it's safe even if the list is sorted or filtered. -
.animation(.default, value: items) — Attaching an animation to the list means SwiftUI plays a slide-out transition whenever
itemschanges, giving users a polished confirmation that the row was removed.
Variants
Undo / Archive action on the leading edge
.swipeActions(edge: .leading) {
Button {
archiveItem(item)
} label: {
Label("Archive", systemImage: "archivebox")
}
.tint(.blue)
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
deleteItem(item)
} label: {
Label("Delete", systemImage: "trash")
}
}
You can stack multiple .swipeActions modifiers — one per edge. SwiftUI renders them left-to-right in declaration order. Apply a custom .tint to override the default grey for non-destructive actions.
SwiftData / @Query integration
When your items come from a @Query property, delete through the ModelContext instead of mutating a local array:
@Environment(\.modelContext) private var modelContext
@Query private var items: [GroceryItem]
// inside swipeActions:
Button(role: .destructive) {
modelContext.delete(item)
} label: {
Label("Delete", systemImage: "trash")
}
Common pitfalls
-
swipeActions requires iOS 15+, but full-swipe confirmation was refined in iOS 17. On iOS 15–16,
allowsFullSwipebehaves identically, but the haptic feedback pattern differs. Since this guide targets iOS 17+, you don't need to branch — just be aware of the minimum deployment target in your project settings. -
Don't combine swipeActions with onDelete. The older
.onDeletemodifier is still supported but conflicts visually withswipeActionson the sameForEach. Pick one approach per list. PreferswipeActionsfor new code — it's more flexible. -
Accessibility label defaults to button title — make it specific. VoiceOver reads "Delete" for every row if you use a bare
Label("Delete", …). Add.accessibilityLabel("Delete \(item.name)")to the button so screen-reader users know which item they're acting on.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement swipe to delete in SwiftUI for iOS 17+. Use swipeActions with role: .destructive on the trailing edge. Make it accessible (VoiceOver labels include the item name). Add a #Preview with realistic sample data.
In Soarias's Build phase, drop this prompt into a screen scaffold and Claude Code will wire the deletion logic directly into your existing @Observable model — no manual plumbing needed.
Related
FAQ
Does swipeActions work on iOS 16?
Yes — swipeActions was introduced in iOS 15. All examples on this page are also compatible with iOS 15 and 16; the iOS 17+ label reflects this guide's minimum target, not an API restriction. Set your deployment target accordingly in Xcode's project settings.
Can I show a confirmation alert before deleting?
Yes. Set allowsFullSwipe: false so the user must tap the button explicitly, then drive a .confirmationDialog or .alert from a piece of @State that stores the pending item. Only call deleteItem once the user confirms.
What's the UIKit equivalent?
In UIKit you implement tableView(_:trailingSwipeActionsConfigurationForRowAt:) returning a UISwipeActionsConfiguration with a UIContextualAction of style .destructive. SwiftUI's swipeActions wraps this UIKit machinery — you get the same system animation and haptics for free.
Last reviewed: 2026-05-11 by the Soarias team.