Upgrading the delete confirmation button

by Andy

Standard delete confirmationiOS has a nice pattern for deleting a row in a table, either tap a minus symbol in a red circle or swipe your finger along the row and a delete button slides in from the right, tap the button to confirm the action or tap elsewhere to cancel. The only problem is that if you have a different look and style than the default then the delete button looks out of place.

I encountered this recently and decided to see if I could replace the delete button with one more in keeping with the rest of the graphic style in my MealSchedule app.

Searching for examples of what other people might have done led to a couple of general approaches. The first was to subclass UITableViewCell to get the ability to override the willTransitionToState: method that is invoked as a cell moves through various editing states, and the second was to walk through the view hierarchy of the newly animated delete button and modify it. There’s nothing wrong with first suggestion because it utilizes a standard API, but the second involves testing against internal class names and assuming a particular view hierarchy, neither of which have any place in any app, regardless of whether you are submitting it to the AppStore or not. Working against an API or utilizing ordinarily hidden features is a bad smell and a strong indication that you are doing something wrong.

So the first step to having a nice delete confirmation button is to subclass UITableViewCell and override willTransitionToState:. The method is called with a bit mask of the new states. We are interested in the situation where the delete confirmation button is about to be displayed.

The default transition will create the shiny delete button and animate it into place. We don’t want to do that because we want to create our own button, but we will leave the other transitions to their default for the time being, so we are careful as to when we invoke super.

The next step is to create the new button and animate it into place. MealSchedule has a UIButton subclass named MPButton that encapsulates my alternative presentation, so let’s extend the transition to include that.

I use an instance variable to hold a reference to the delete button. I don’t strictly need to do this – I could use a tag and viewWithTag: instead, but the default implementation of the delete confirmation button uses tags within its hierarchy and so keeping a reference is safer than a tag by avoiding any unintended side effects from the base classes looking for views. The RBRectCenterPositionY is a simple function I use to center a CGRect within a particular height. I also use a category on UIScreen to return various numbers, such as the standard 10 pixels that are added as padding to a table’s content.

Note that when the cell transitions out of the delete confirmation state we want to make sure the delete button is moved out of the way, though if the cell is being deleted (as flagged by the _isDeleting variable) when just fade the button to invisible rather than move it to the right. This works as a smoother effect during the actual delete, which we will see in a minute, but for now you can tap the red minus to animate the delete button in, and tap it again to animate the button away.

The button is worthless unless we can actually perform a delete so we need to provide a method for the action we specified in the button creation.

When the delete button is tapped, it will invoke the delete: method that in turn invokes another local method named commitEdit:. As my button is a complete replacement for the standard button then we need to invoke the same transitions that would normally occur. In this particular situation, my table is always in edit mode so we will always be transitioning back to the UITableViewCellStateShowingEditControlMask state.

The table’s datasource is the class that handles the delete so we need to invoke the standard tableView:commitEditingStyle:forRowAtIndexPath: method. The only slight problem here is that we need to know our current index path, and that is not available to us from the perspective of our UITableViewCell world. Unfortunately neither is the knowledge of the containing table. The table is actually a piece of private information in the base UITableViewCell class, but is not exposed to subclasses. Therefore we need to create a property, named table here, to pass in the table reference when we create an instance of our cell. Note that you cannot call this property tableView because that will conflict with the private reference!

The final step is to handle the cancelation of the delete by tapping elsewhere in the table. The easiest way of doing that is to set up a UITapGestureRecognizer on the table.

The gesture recognizer is created when we transition into the delete confirmation state, and removed when we have transition out of the confirmation.

One small wrinkle that I discovered here was a difference between iOS5 and iOS6. In the later SDK, the tap on the delete button will override that of the tap gesture, meaning the delete: method is invoked directly, but on iOS5 (and possibly earlier but I am not targeting those platforms so I didn’t test them), the tap gesture fires and the button’s action does not. Hence the deleteConfirmation: method tests to see if the tap was actually over the delete button or not.

Custom delete confirmationNow I have a nice delete confirmation button that matches all the other buttons in the app, and has been implemented without the knowledge or use of internal classes or views. In addition to the code above, I added a couple of custom methods on my table delegate to dim the other visible controls to avoid any confusion over the delete action, but that is just a little detail in my particular implementation.

I also decided to leave the red minus as is because it doesn’t look as out of place as much as the confirmation button. The only functionality I have not replicated from the default delete confirmation process is animating the red minus back to horizontal if the delete is cancelled. To be able to perform that animation with the standard button would require referencing the internal representation. Alternatively I could replace the button, which I might do if the lack of animation starts to bug me.