Reputation: 1943
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]
}
Upvotes: 9
Views: 6385
Reputation: 285059
There are two important rules:
reloadData()
right after insert/move/deleteRows...
, the insert/move/delete operation reorders the table and does the animation.insert/move/deleteRows...
always after changing the data source array.Upvotes: 16