UITableView
iOS Development
Swift
Swipe to Delete
UITableViewCell

Add swipe to delete UITableViewCell

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Swipe-to-delete is one of the most recognizable gestures in iOS. Users expect it in any list-based interface, from email inboxes to to-do apps. UITableView has built-in support for this interaction, and understanding how it works — from the basic delegate method to fully customized swipe actions — will help you build polished, intuitive table views.

Basic Swipe-to-Delete with commit editingStyle

The simplest way to enable swipe-to-delete is to implement a single UITableViewDataSource method. When you provide this method, UITableView automatically adds a red "Delete" button that appears when the user swipes left on a cell:

swift
1class TaskListViewController: UITableViewController {
2    var tasks = ["Buy groceries", "Walk the dog", "Write report", "Call dentist"]
3
4    override func tableView(
5        _ tableView: UITableView,
6        numberOfRowsInSection section: Int
7    ) -> Int {
8        return tasks.count
9    }
10
11    override func tableView(
12        _ tableView: UITableView,
13        cellForRowAt indexPath: IndexPath
14    ) -> UITableViewCell {
15        let cell = tableView.dequeueReusableCell(
16            withIdentifier: "TaskCell",
17            for: indexPath
18        )
19        cell.textLabel?.text = tasks[indexPath.row]
20        return cell
21    }
22
23    // This single method enables swipe-to-delete
24    override func tableView(
25        _ tableView: UITableView,
26        commit editingStyle: UITableViewCell.EditingStyle,
27        forRowAt indexPath: IndexPath
28    ) {
29        if editingStyle == .delete {
30            tasks.remove(at: indexPath.row)
31            tableView.deleteRows(at: [indexPath], with: .automatic)
32        }
33    }
34}

The critical detail here is the order of operations: you must update your data source first, then call deleteRows(at:with:). If the data source count does not match the expected count after the deletion, your app will crash with an internal consistency exception.

Custom Trailing Swipe Actions

For anything beyond a plain delete button, use trailingSwipeActionsConfigurationForRowAt. This delegate method lets you define multiple actions with custom titles, colors, and images:

swift
1override func tableView(
2    _ tableView: UITableView,
3    trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
4) -> UISwipeActionsConfiguration? {
5
6    // Delete action
7    let deleteAction = UIContextualAction(
8        style: .destructive,
9        title: "Delete"
10    ) { [weak self] _, _, completionHandler in
11        self?.tasks.remove(at: indexPath.row)
12        tableView.deleteRows(at: [indexPath], with: .automatic)
13        completionHandler(true)
14    }
15    deleteAction.image = UIImage(systemName: "trash")
16
17    // Archive action
18    let archiveAction = UIContextualAction(
19        style: .normal,
20        title: "Archive"
21    ) { [weak self] _, _, completionHandler in
22        self?.archiveTask(at: indexPath)
23        completionHandler(true)
24    }
25    archiveAction.backgroundColor = .systemOrange
26    archiveAction.image = UIImage(systemName: "archivebox")
27
28    let configuration = UISwipeActionsConfiguration(
29        actions: [deleteAction, archiveAction]
30    )
31    // Prevent full swipe from triggering the first action automatically
32    configuration.performsFirstActionWithFullSwipe = false
33    return configuration
34}

The style: .destructive parameter is important because it tells UIKit this action removes the cell. UIKit uses this information to animate the cell removal correctly. Non-destructive actions use style: .normal and keep the cell visible after the action completes.

Leading Swipe Actions

You can also add actions that appear when the user swipes from left to right. This is useful for positive actions like marking items as favorites or marking messages as read:

swift
1override func tableView(
2    _ tableView: UITableView,
3    leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath
4) -> UISwipeActionsConfiguration? {
5
6    let favoriteAction = UIContextualAction(
7        style: .normal,
8        title: "Favorite"
9    ) { [weak self] _, _, completionHandler in
10        self?.toggleFavorite(at: indexPath)
11        completionHandler(true)
12    }
13    favoriteAction.backgroundColor = .systemYellow
14    favoriteAction.image = UIImage(systemName: "star.fill")
15
16    let markReadAction = UIContextualAction(
17        style: .normal,
18        title: "Read"
19    ) { [weak self] _, _, completionHandler in
20        self?.markAsRead(at: indexPath)
21        completionHandler(true)
22    }
23    markReadAction.backgroundColor = .systemBlue
24
25    return UISwipeActionsConfiguration(
26        actions: [favoriteAction, markReadAction]
27    )
28}

Destructive vs Non-Destructive Actions

The distinction between destructive and non-destructive actions affects both appearance and behavior:

swift
1// Destructive: red background by default, removes the cell on full swipe
2let delete = UIContextualAction(style: .destructive, title: "Delete") {
3    _, _, completionHandler in
4    // Remove from data source and table view
5    completionHandler(true) // true = action was performed, cell is removed
6}
7
8// Non-destructive: gray background by default, cell stays visible
9let mute = UIContextualAction(style: .normal, title: "Mute") {
10    _, _, completionHandler in
11    // Update the item state
12    completionHandler(true) // true = action was performed, swipe resets
13}
14mute.backgroundColor = .systemPurple

When you call completionHandler(true) on a destructive action, UIKit animates the cell off screen. Calling completionHandler(false) indicates the action failed and the cell slides back to its original position.

Updating the Data Source Safely

The most crash-prone part of swipe-to-delete is keeping the data source in sync with the table view. Always wrap multiple changes in performBatchUpdates to avoid inconsistency crashes:

swift
1func deleteMultipleItems(at indexPaths: [IndexPath]) {
2    // Sort in reverse order to avoid index shifting issues
3    let sorted = indexPaths.sorted { $0.row > $1.row }
4
5    tableView.performBatchUpdates {
6        for indexPath in sorted {
7            tasks.remove(at: indexPath.row)
8        }
9        tableView.deleteRows(at: indexPaths, with: .automatic)
10    }
11}

If you are using a UITableViewDiffableDataSource (iOS 13+), the snapshot-based approach eliminates these index management issues entirely:

swift
var snapshot = dataSource.snapshot()
snapshot.deleteItems([itemToDelete])
dataSource.apply(snapshot, animatingDifferences: true)

Common Pitfalls

  • Updating the table view before the data source: Always remove the item from your array or database first, then call deleteRows(at:with:) — reversing this order causes a crash.
  • Forgetting to call the completion handler: Every UIContextualAction handler receives a completionHandler closure that you must call, or the swipe UI will freeze in place.
  • Not using [weak self] in action closures: The closure captures self strongly by default, which can create retain cycles with the view controller.
  • Allowing full swipe on dangerous actions: Set performsFirstActionWithFullSwipe = false on the configuration to prevent accidental deletions from a fast swipe gesture.
  • Ignoring accessibility: Swipe actions are not discoverable by VoiceOver users by default — implement accessibilityCustomActions on your cells so all users can access these features.

Summary

  • Implement tableView(_:commit:forRowAt:) for basic swipe-to-delete with minimal code.
  • Use trailingSwipeActionsConfigurationForRowAt and leadingSwipeActionsConfigurationForRowAt for custom swipe actions with colors, images, and multiple buttons.
  • Set style: .destructive for actions that remove the cell and style: .normal for actions that keep it visible.
  • Always update your data source before telling the table view to delete rows to avoid internal consistency crashes.
  • Use performsFirstActionWithFullSwipe = false to prevent accidental full-swipe deletions on destructive actions.

Course illustration
Course illustration

All Rights Reserved.