Fix: Swipe to Delete Not Showing on UITableViewCell in Swift

Fix: Swipe to Delete Not Showing on UITableViewCell in Swift

Written by

in

Quick answer: To enable swipe-to-delete on a UITableViewCell, implement tableView(_:commit:forRowAt:) in your UITableViewDataSource. Alternatively, implement tableView(_:trailingSwipeActionsConfigurationForRowAt:) in your UITableViewDelegate using a destructive UIContextualAction.

Why this happens

UITableView does not display a delete button on horizontal swipe by default. UIKit only enables swipe gestures when the table view’s data source or delegate explicitly confirms that the row supports editing.

The most common cause is omitting tableView(_:commit:forRowAt:) from your UITableViewDataSource. Even if tableView(_:canEditRowAt:) returns true, UIKit ignores swipe gestures unless the data source also provides the commit handler to process the deletion.

Secondary causes include conflicting table view configurations and gesture recognizer collisions. If tableView.isEditing is already active, normal swipe gestures are suppressed in favor of editing controls. Similarly, setting allowsMultipleSelectionDuringEditing = true without proper editing mode handling, or attaching custom UIPanGestureRecognizer instances to cell content views, intercepts horizontal touches before the table view can process them.

How to fix it

1. Implement tableView(_:commit:forRowAt:)

The standard UIKit mechanism for swipe-to-delete requires implementing tableView(_:commit:forRowAt:) in your UITableViewDataSource. Once this method exists, UIKit automatically renders the red “Delete” button on trailing swipe.

You must update your underlying data array before modifying the table view rows. Failing to delete the model item first causes an NSInternalInconsistencyException crash.

import UIKit

class ItemsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    @IBOutlet weak var tableView: UITableView!

    var items = ["Alpha", "Bravo", "Charlie", "Delta"]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath)
        cell.textLabel?.text = items[indexPath.row]
        return cell
    }

    // Required for the default swipe-to-delete behavior
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            // 1. Remove the item from your data source
            items.remove(at: indexPath.row)

            // 2. Animate the row deletion from the table view
            tableView.deleteRows(at: [indexPath], with: .automatic)
        }
    }
}

2. Use trailingSwipeActionsConfigurationForRowAt for modern actions

For more control over the action title, background color, or icon, implement tableView(_:trailingSwipeActionsConfigurationForRowAt:) in your UITableViewDelegate. This method overrides the legacy editing style commit workflow and allows custom action styling.

extension ItemsViewController {
    func tableView(
        _ tableView: UITableView,
        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {

        let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { [weak self] (_, _, completionHandler) in
            guard let self = self else {
                completionHandler(false)
                return
            }

            // Remove from data source
            self.items.remove(at: indexPath.row)

            // Animate deletion from table view
            tableView.deleteRows(at: [indexPath], with: .fade)

            // Signal completion to dismiss the swipe view
            completionHandler(true)
        }

        deleteAction.image = UIImage(systemName: "trash")

        let configuration = UISwipeActionsConfiguration(actions: [deleteAction])
        configuration.performsFirstActionWithFullSwipe = true

        return configuration
    }
}

3. Verify canEditRowAt and selection flags

If you implemented tableView(_:canEditRowAt:), ensure it returns true for rows that should support deletion. By default, UIKit treats all rows as editable when this method is omitted, but an explicit return false disables the swipe action entirely.

Also check your view controller configuration during viewDidLoad. Make sure multiple selection settings are not disabling swipe actions:

override func viewDidLoad() {
    super.viewDidLoad()

    // Ensure dataSource and delegate are assigned
    tableView.dataSource = self
    tableView.delegate = self

    // Prevent editing modes from locking swipe actions
    tableView.allowsMultipleSelectionDuringEditing = false
    tableView.setEditing(false, animated: false)
}

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    // Ensure this returns true for rows you want to delete
    return true
}

4. Check for gesture recognizer conflicts

If custom views inside your cell handle touch events, they may block the internal swipe gesture of UITableView. Avoid placing custom horizontal UIPanGestureRecognizer instances on the cell’s contentView.

If you must use custom gestures inside a cell, allow simultaneous recognition by conforming to UIGestureRecognizerDelegate:

func gestureRecognizer(
    _ gestureRecognizer: UIGestureRecognizer,
    shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer
) -> Bool {
    return true
}

How to prevent it

  • Assign both tableView.dataSource and tableView.delegate in code or Interface Builder before expecting editing callbacks.
  • Never update the table view UI with deleteRows(at:with:) without mutating the underlying collection first.
  • Prefer trailingSwipeActionsConfigurationForRowAt when building modern UIKit apps for granular control over swipe action appearance.
  • Do not toggle tableView.isEditing = true globally if your intention is solely to allow on-demand swipe actions on individual cells.

FAQ

Do I need to implement both commit editingStyle and trailingSwipeActionsConfiguration?

No. If you implement tableView(_:trailingSwipeActionsConfigurationForRowAt:), UIKit prioritizes it and ignores tableView(_:commit:forRowAt:). Use the modern swipe configuration API whenever possible.

Why does my app crash when tapping the delete button?

This crash occurs when your data model count does not match the row count after deletion. Always remove the element from your array before calling deleteRows(at:with:) so that subsequent calls to tableView(_:numberOfRowsInSection:) return the updated count.

Why does swipe to delete stop working when isEditing is set to true?

When tableView.isEditing is active, the table view enters batch editing mode, showing left-hand selection circles or reorder handles. Trailing swipe gestures are disabled in this mode by design.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.