Ad

Our DNA is written in Swift
Jump

Detecting Taps Outside of Tableview Cells

In the latest version of iWoman– which I’ve been working on the past few weeks – I had a situation where I am sliding up a date picker when the user taps on a date cell. Sliding it out when the user taps on other cell was easy, you can do that in the other cells’ didSelectRowAtIndexPath. But if you have that in a grouped tableview then there are several areas outside of cells that also could become the targets of a user’s taps.

Generally a user would also assume to be able to dismiss the picker by tapping there, in headers or empty areas where you see the background shine through. In this blog post I’m showing you a technique on how this is done most efficiently and also backwards compatible.

I tried out several approaches before settling on this solution. You might be able to use a gesture recognizer, but that would rule out 3.1 compatibility. You could override the touch methods of the table view and do some fancy detective work there. But I found – as I did so often before – that by far the most convient way is to override hitTest.

Every UIView has a method hitTest:withEvent: that enables the OS to ask the views if the user tapped on one of their subviews or the view itself. If the user tapped into a subview then the hitTest will return a pointer to this view if this subview has userInteraction enabled. If not or there is nothing under the finger except the view itself then it will return itself. Or if the view was not actually hit then it will return nil.

In order to override a tableview’s hitTest you have to sub-class it. So I have a new DTTableView class that does just that.

DTTableView.h

@interface DTTableView : UITableView
{
}
 
@end

DTTableView.m

#import "DTTableView.h"
 
@implementation DTTableView
 
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
	// check if a row was hit
	NSIndexPath *indexPath = [self indexPathForRowAtPoint:point];
 
	if (!indexPath)
	{
		[[NSNotificationCenter defaultCenter] postNotificationName:@"DTTableViewHitOutsideCell" object:self];
	}
 
	return [super hitTest:point withEvent:event];
}
 
@end

This code is run every time a hit is checked on the tableview. The indexPathForRowAtPoint will return an index path if indeed a cell was hit. Otherwise it will return nil. This we can use to send out an app-wide notification that would trigger any non-cell behavior that we wish for.

Another handy trick with hitTest I’ve used on several occasions where I had problems to figure out which views are actually receiving my hits. Just store the return value from [super hitTest:] and NSLog it. The resulting log output will give you very enlightening descriptions of the hit views.

Finally there’s another thing you can do with hitTest: you can make a view behave more selectively. Like you can have some areas be impervious to hits, like transparent areas. Returning nil after having determined that the hit should be ignored will cause the OS go search elsewhere in the view hierarchy for a recipient and the hit would then go to an underlying/shining-through view below the ignoring view.

Sure you could use some touch  analysis, but I suspect that there is some touch bending or exclusivity when dealing with table views or scroll views. Therefore this suggested approach is simple and works.


Categories: Recipes

2 Comments »

  1. Thanks for the great information! Very useful.

  2. Very clean solution. Here is my slight variation on this in Swift:

    @objc protocol ChecklistTableViewDelegate : UITableViewDelegate {
    func tableViewDidTapBelowCells(tableView:UITableView);
    }

    class ChecklistTableView: UITableView {
    override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    let indexPath = self.indexPathForRowAtPoint(point)

    if indexPath == nil {
    let delegate = self.delegate as ChecklistTableViewDelegate
    delegate.tableViewDidTapBelowCells(self)
    }

    return super.hitTest(point, withEvent: event)
    }
    }