```html SwiftUI: How to Swipe to Delete (iOS 17+, 2026)

How to implement swipe to delete in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: swipeActions Updated: May 11, 2026
TL;DR

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

  1. ForEach + Identifiable — The list iterates over GroceryItem values that conform to Identifiable. SwiftUI uses the stable id to animate removals correctly without index bookkeeping.
  2. .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.
  3. Button(role: .destructive) — Passing role: .destructive automatically tints the swipe slot red and tells VoiceOver the action is irreversible. You get correct semantics with zero styling code.
  4. deleteItem(_:) — The helper calls removeAll using the item's id rather than an index, so it's safe even if the list is sorted or filtered.
  5. .animation(.default, value: items) — Attaching an animation to the list means SwiftUI plays a slide-out transition whenever items changes, 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

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.

```