UIContextualAction with destructive style seems to delete row by default
Asked Answered
E

4

18

The problem I'm seeing is that when I create a UIContextualAction with .destructive and pass true in completionHandler there seems to be a default action for removing the row.

If you create a new Master-Detail App from Xcode's templates and add this code in MasterViewController...

override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let testAction = UIContextualAction(style: .destructive, title: "Test") { (_, _, completionHandler) in
        print("test")
        completionHandler(true)
    }
    return UISwipeActionsConfiguration(actions: [testAction])
}

the row you swipe will be removed. Notice that there's no code there updating the table view. Also the model is not updated and if you scroll way up to cause the row to be reloaded it will reappear.

Passing false in this case does not remove the row. Or using the .normal style and true also does not remove the row.

.destructive and true results in the row being removed by default.

Can anyone explain this behaviour? Why is the row being removed?

Epididymis answered 4/11, 2017 at 0:7 Comment(0)
P
18

Per the documentation for the Destructive option:

An action that deletes data or performs some type of destructive task.

The completion is meant to signify if the action was a success. Passing true would mean that the destructive task was a success and thus the row should be removed.

You are meant to manually update your dataSource when the destructive action occurs and not doing so would cause scrolling to make the data reappear. You will also need to tell the tableView that the data has been deleted.

Below is some code showing a working example:

UIContextualAction(style: .destructive, title: "Delete") { [weak self] (_, _, completion) in
    if self?.canDelete(indexPath) { // We can actually delete
        // remove the object from the data source
        self?.myData.remove(at: indexPath.row)
        // delete the row.  Without deleting the row or reloading the
        // tableview, the index will be off in future swipes
        self?.tableView?.deleteRows(at: [indexPath], with: .none)
        // Let the action know it was a success.  In this case the 
        // tableview will animate the cell removal with the swipe
        completion(true)    
    } else { // We can't delete for some reason
        // This resets the swipe state and nothing is removed from
        // the screen visually.
        completion(false)
    }         
}

Then I need to reload the tableview or call deleteRows in order to have the indexPath be properly computed on the next swipe.

If I have 10 rows and I swipe the 5th one to delete, every one after that will be off by one row unless the tableview is reloaded or the tableview is told of the row being removed in some way.

Phone answered 21/2, 2018 at 5:31 Comment(1)
Following this code I get a crash. The reason is -- you shouldn't be deleting the row by yourself in this line self?.tableView?.deleteRows(at: [indexPath], with: .none). The UIKit does the deleting row when you call completion(true)Cirenaica
S
5

I can't reproduce this issue in iOS 13. Either the behavior in iOS 12 and before was a bug or it has simply been withdrawn (perhaps because it was confusing).

In iOS 13, if you just return from a .destructive action by calling completion(true) without doing anything, nothing happens and that's the end of the matter. The cell is not deleted for you.

Stalemate answered 28/8, 2019 at 5:2 Comment(1)
Its true that the behaviour is different for iOS12 and iOS13, and I thought this is a bug, github.com/xmartlabs/Eureka/issues/1923Hounding
H
2

I agree with the answer by Kris Gellci, but notice that if you are using a NSFetchedResultsController it may complicate things. It seems that for a destructive UIContextualAction the call completion(true) will delete the row, but so may the NSFetchedResultsController's delegate. So you can easily end up with errors in that way. With NSFetchedResultsController I decided to call completion(false) (to make the contextual menu close), regardless of whether the action was a success or not, and then let the delegate take care of deleting the table row if the corresponding object has been deleted.

Humes answered 28/4, 2019 at 22:6 Comment(0)
S
0

Use the below flag for disabling full swipe.

performsFirstActionWithFullSwipe

By default it will automatically performs the first action which is configured in UISwipeActionsConfiguration. so if you want to disable full swipe delete then set "performsFirstActionWithFullSwipe" as false.

func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let test = UIContextualAction(style: .destructive, title: "test") { (action, view, completion) in
        
        // Your Logic here
        completion(true)
    }
    
    let config = UISwipeActionsConfiguration(actions: [test])
    config.performsFirstActionWithFullSwipe = false
    return config
} 

Hope this will solve your problem.

Sumbawa answered 20/4, 2021 at 19:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.