The dreaded double-segue mistake

This turns out to be a surprisingly common mistake with table views and collection views:

  1. In the storyboard, you connect a segue from the cell prototype to another view controller.

  2. In code, you implement the delegate method didSelectRowAt or didSelectItemAt to call performSegue to trigger the segue.

The result of this mistake is that when the user taps the cell, the segue is triggered twice.

The reason is that the connection of the segue from the cell prototype in the storyboard makes this an action segue, meaning that the segue is triggered automatically when the user taps the cell. But then you are also triggering the segue manually by calling performSegue.

This mistake is remarkably difficult to track down. It usually manifests itself through some secondary effect; for example, you crash in your implementation of prepare when you try to configure the destination view controller based on a value that you thought would be set properly but isn’t.

In this example from Stack Overflow, the programmer is crashing in prepare when an array instance variable that he sets in didSelect is mysteriously not set when he gets to prepare:

var selectedStores = [String]()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    selectedStores.append(stores[indexPath.row])
    performSegue(withIdentifier: "showStoreDetailsSegue", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "showStoreDetailsSegue" {
        let destinationVC = segue.destination as! StoreDetailsViewController
        destinationVC.selectedStore = selectedStores[0] // crash!
    }
}

In didSelect, he sets the selectedStores instance variable and calls performSegue. So then when prepare is called, he checks that this is the same segue, he tries to retrieve the value he put into selectedStore, and it’s not there.

The reason is that this call to prepare is not the call that was triggered by performSegue in didSelect. It is the call that is triggered by the segue in the storyboard, automatically, when the user taps the cell. At that time, didSelect has not been called yet, so selectedStores has not been set yet.

There are many other variants on what can go wrong, but the root cause is the double segue.

There are two ways to fix the problem:

  • Instead of the segue emanating from the prototype cell, have it emanate from the view controller itself. That way, it won’t trigger automatically when the user taps the cell, and you are safe to trigger it manually in didSelect.

  • Or, do just the opposite: leave the segue as it is, emanating from the prototype cell, and delete your implementation of didSelect. You probably didn’t need it to begin with.

(In the above example, for instance, there is no need for the selectedStores variable in the first place. All the work could easily have been done in prepare.)