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.dataSourceandtableView.delegatein 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
trailingSwipeActionsConfigurationForRowAtwhen building modern UIKit apps for granular control over swipe action appearance. - Do not toggle
tableView.isEditing = trueglobally 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.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
