Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Crash with tableView.deleteRows(at:, with:)

I have an erratic problem with an app I'm developing. I have an array of tracks that is displayed in a table view. In this table view a user can swipe a single track left and a menu with three items appears. One of which is a delete action. It is with this action I have a problem.

The menu is implemented in the UITableViewDelegate method:

override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?

I retrieve the selected row with the following statement:

selectedTrack = indexPath.row

Then I make a delete action with this snippet:

//Delete the selected track
let deleteAction = UITableViewRowAction(style: .default, title: deleteTitle) { (action, indexPath) -> Void in

    if self.savedTracks.count > 0 {
        self.newTrack = self.savedTracks[self.selectedTrack]
        let albumName = (self.newTrack?.trackName)! + String(describing: (self.newTrack?.trackDate)!)
        self.savedTracks.remove(at: self.selectedTrack)
        self.trackOverviewTable.deleteRows(at: [indexPath], with: .fade)
        self.photoStore.deleteAlbumFromDeletedTrack(with: albumName)
        self.trackOverviewTable.reloadData()
        self.saveTracksToDisk()
    } else {
        self.trackOverviewTable.reloadData()
    }

The program runs fine, but if I delete two rows after another, the program crashes with the comment: libc++abi.dylib: terminating with uncaught exception of type NSException.

If I comment out (or delete) the line:

self.trackOverviewTable.deleteRows(at: [indexPath], with: .fade)

The app runs fine, but the animation of deleting the row is gone. The funny thing is that the app sometimes runs fine with the "deleteRows" function, but often not. I've looked at several comments, but I have not yet found the right answer.

The table has no sections, so the data source method:

numberOfSections(in: UITableView)

is not implemented. Can anyone help me in the right direction?

Based on the advice of Vadian I tried to change the code snippet to:

    //Delete the selected track
    let deleteAction = UITableViewRowAction(style: .default, title: deleteTitle) { (action, indexPath) -> Void in

        self.trackOverviewTable.beginUpdates()
        self.newTrack = self.savedTracks[self.selectedTrack]
        let albumName = (self.newTrack?.trackName)! + String(describing: (self.newTrack?.trackDate)!)
        self.savedTracks.remove(at: indexPath.row)
        self.photoStore.deleteAlbumFromDeletedTrack(with: albumName)
        self.saveTracksToDisk()
        self.trackOverviewTable.deleteRows(at: [indexPath], with: .fade)
        self.trackOverviewTable.endUpdates()
    }

This, however, still makes the app crash.

The problem with the app crash was that I used a complex and odd way to fill the table view, which ocassionally made the app expect a different number of table rows then there really were. The code that now works looks like this:

/** This method provides the actions when the user swipes a row left. The method first checks what sort of track it is. If the track is an emptyTrack or 
 instructionTrack, there will be no action that can be chosen. When the track is a regular recorded track the method will provide the user with the option
 to post the selected track or delete the track.
*/
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {

    let faultTitle = NSLocalizedString("No tracks.", comment: "The message in the selection when there are no tracks.")
    let postTitle = NSLocalizedString("Post track.", comment: "The message in the selection for posting the track on social media.")
    let deleteTitle = NSLocalizedString("Delete.", comment: "The message in the selection for deleting the track.")

    selectedTrack = indexPath.row
    let selectedTrackName = self.savedTracks[selectedTrack].trackName

    //When the user swiped the empty or instruction track, only provide the fault action.
    guard (selectedTrackName != self.emptyString) && (selectedTrackName != self.instructionString) else {
        //The user selected the empty or instruction track, the action cannot be executed.
        let faultAction = UITableViewRowAction(style: .normal, title: faultTitle, handler: { (action, indexPath) -> Void in

            self.trackOverviewTable.reloadData() //Simply reload the table view after the action button is hit.
        })

        self.trackOverviewTable.cellForRow(at: indexPath)?.isSelected = false
        return [faultAction]
    }

    //The post action first checks whether the user had bought the fll version.
    //If the full version is bought the action moves to the posting menu.
    let postAction = UITableViewRowAction(style: .normal, title: postTitle) { (action, indexPath) -> Void in

        //The message when the full version has not been bought
        let alertTitle = NSLocalizedString("Upgrade @Tracker", comment: "The user doesn't have the full capabilities.")
        let alertMessage = NSLocalizedString("Do you want to upgrade @Tracker to unlock all functionalities?", comment: "Ask the user to buy the functionalities.")

        //The new track will now be set to be the selected track so it can be transfered to
        //the posting view controller
        self.newTrack = self.savedTracks[self.selectedTrack]

        if FULL_FUNCTIONALITY == true {
            self.performSegue(withIdentifier: "postTrack", sender: self)
        } else {
            //Allow the user to post the track if the full functionality is bought
            self.alertMessage(title: alertTitle, message: alertMessage, action: "Buy")
        }
    }

    //Delete the selected track
    let deleteAction = UITableViewRowAction(style: .default, title: deleteTitle) { (action, indexPath) -> Void in

        //First set the warning message when the user wants to delete the track.
        let title = NSLocalizedString("Warning", comment: "Ask alert the user that he is going to delete the track.")
        let message = NSLocalizedString("Are you sure you want to delete the track?", comment: "Ask the user if he's sure.")
        let alertYes = NSLocalizedString("Yes", comment: "Ja")
        let alertNo = NSLocalizedString("No", comment: "Nee")
        let alertMessage = UIAlertController.init(title: title, message: message, preferredStyle: .alert)

        let okAction = UIAlertAction(title: alertYes, style: .default, handler: { (action) in
            self.deleteTrackFromTable(at: indexPath)

            var indexPathSelected = indexPath
            indexPathSelected.row = 0
            self.trackOverviewTable.scrollToRow(at: indexPathSelected, at: .none, animated: true)
            self.presentingViewController?.dismiss(animated: true, completion: nil)
        })
        alertMessage.addAction(okAction)

        let notOkAction = UIAlertAction(title: alertNo, style: .default, handler: { (action) in
            self.trackOverviewTable.reloadData()
            self.presentingViewController?.dismiss(animated: true, completion: nil)
        })
        alertMessage.addAction(notOkAction)

        self.present(alertMessage, animated: true, completion: nil)
    }

    postAction.backgroundColor = MENU_COLOR_1

    self.trackOverviewTable.cellForRow(at: indexPath)?.isSelected = false
    return [deleteAction, postAction, analysisAction]
}
like image 493
MacUserT Avatar asked Jan 07 '17 19:01

MacUserT


1 Answers

There are two important rules:

  • Never call reloadData() right after insert/move/deleteRows..., the insert/move/delete operation reorders the table and does the animation.
  • Call insert/move/deleteRows... always after changing the data source array.
like image 139
vadian Avatar answered Oct 03 '22 10:10

vadian