Rumin
Rumin

Reputation: 3790

Intermittent Crash: Attempt to delete row 0 from section 0 which only contains 0 rows before the update

We have implemented swipe to delete feature for our app, but somehow we are seeing this intermittent prod crash on Crashlytics.

I have been following several StackOverflow posts about this crash, but I am unable to get the exact cause of this crash.

I have tried producing this crash several times but every time it works fine.

Any thoughts or ideas, if anything I am doing wrong here? Below is the crash report and the code snippet which is currently live.

enter image description here

@available(iOS 11.0, *)
private func actionForType(alertID: String, swipeAction: AlertSwipeActionType, indexPath: IndexPath) -> UIContextualAction {
    let contexualAction = UIContextualAction(style: .normal, title: nil) { [weak self] (action, view, completion) in

        guard let strongSelf = self else {
            return
        }

        switch swipeAction {
        ......
        case .affirm:
            completion(true)
            strongSelf.dispositionAlert(id: alertID, status: true, indexPath: indexPath)
        ......
        }
    }

    ......
    return contexualAction
}

fileprivate func dispositionAlert(id: String, status: Bool, indexPath: IndexPath) {
    let dispositionRequest = AlertDispositionUpdateBody(id: id, disposition: status, questionId: nil)
    self.updateAlertDispositionStatus(request: dispositionRequest) { [weak self] in

        guard let strongSelf = self else {
            return
        }
        strongSelf.removeCellWithAnimationAt(indexPath: indexPath)
        strongSelf.loadAlerts()
    }
}

fileprivate func removeCellWithAnimationAt(indexPath: IndexPath) {
    DispatchQueue.main.async {
        self.tableView.beginUpdates() // likely not required
        self.removeAlertAtIndexPath(indexPath)
        self.tableView.deleteRows(at: [indexPath], with: .fade)
        self.tableView.endUpdates() // likely not required either
    }
}

@objc func loadAlerts() {
    self.startLoadingAnimation()
    self.alertsFooterList.removeAll()

    AlertsManager.sharedInstance.loadMemberAlerts()
}

fileprivate func removeAlertAtIndexPath(_ indexPath: IndexPath) {
    let alertStatus = self.alertTabType.statusToLoad[indexPath.section]
    if let alerts = self.alertsList[alertStatus], 
       alerts.count > indexPath.row {
       self.alertsList[alertStatus]?.remove(at: indexPath.row)
    }
}

Upvotes: 2

Views: 4970

Answers (2)

Jon Rose
Jon Rose

Reputation: 8563

The crash is telling you what happened: when you try to delete the row it has already been removed. Now that seems strange because the row was there when you presented the alertView. So between the time of presenting the alertView and deleting the row, the row was removed by some other source. Reading your code it is clearly possible. There are two delays between presenting the alertview and deleting the row. The first is a possibly very long time until the user confirms to delete, and the second is the DispatchQueue.main.async which is generally pretty fast, but can still cause these types of bugs.

Instead of recording which indexPath to delete (because the indexPath may be change by the time the user confirms), record the ID of the item you are trying to delete. When the user eventually confirm the alert then lookup where it is in the tableview and delete it if you find it.

The deeper problem is that you have two sources of truth - the tableview and your datasource and they are not sync. It is better to update your datasource first, and then have code that automatically syncs the tableview. This way they always remain in sync. https://github.com/Instagram/IGListKit is a solution that already implements this, and you might get value by using it.

Upvotes: 5

Pradip Patel
Pradip Patel

Reputation: 348

If you use tableview delete row called then first call number of section. below code.

Code: swift

fileprivate func removeCellWithAnimationAt(indexPath: IndexPath) {
    DispatchQueue.main.async {
        self.removeAlertAtIndexPath(indexPath)
        self.tableView.numberOfRows(inSection: indexPath.section) // called first
        self.tableView.deleteRows(at: [indexPath], with: .fade)
    }
}

I Hope this code work.

Upvotes: 0

Related Questions