37

How can I detect a double tap on a certain cell in UITableView?

i.e. I want to perform one action if the user made a single touch and another if a user made a double touch? I also need to know an index path where the touch was made.

How can I achieve this goal?

Thanks.

Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
Ilya Suzdalnitski
  • 52,598
  • 51
  • 134
  • 168
  • 1
    you mean a double tap or a multi-touch? – lostInTransit Jun 23 '09 at 08:41
  • 1
    just from a HIG standpoint, you might want to consider using an accessory button instead of requiring double tap? I don't know the usage scenario, but you might have to explain this one to your users. – Ben Gotow Jun 23 '09 at 13:18
  • The modern approach would be to use a UIGestureRecognizer. Add it in Interface Builder. You can set the number of taps. You connect the UIGestureRecognizer in a target/action way, as you would a button. – TyR Oct 27 '18 at 04:54

16 Answers16

31

If you do not want to create a subclass of UITableView, use a timer with the table view's didSelectRowAtIndex:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    //checking for double taps here
    if(tapCount == 1 && tapTimer != nil && tappedRow == indexPath.row){
        //double tap - Put your double tap code here
        [tapTimer invalidate];
        [self setTapTimer:nil];

        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Double Tap" message:@"You double-tapped the row" delegate:self cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
        [alert show];
        [alert release];
    }
    else if(tapCount == 0){
        //This is the first tap. If there is no tap till tapTimer is fired, it is a single tap
        tapCount = tapCount + 1;
        tappedRow = indexPath.row;
        [self setTapTimer:[NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(tapTimerFired:) userInfo:nil repeats:NO]];
    }
    else if(tappedRow != indexPath.row){
        //tap on new row
        tapCount = 0;
        if(tapTimer != nil){
            [tapTimer invalidate];
            [self setTapTimer:nil];
        }
    }
}

- (void)tapTimerFired:(NSTimer *)aTimer{
    //timer fired, there was a single tap on indexPath.row = tappedRow
    if(tapTimer != nil){
        tapCount = 0;
        tappedRow = -1;
    }
}

HTH

Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
lostInTransit
  • 70,519
  • 61
  • 198
  • 274
  • Would you mind posting more of your code. Where are you setting the tapTimer, tappedRow, and tapCount? Thanks! – Jonah Jan 17 '10 at 16:26
  • Is it safe to set the timer to the magic number 0.2? Is that the usual time limit between taps of a double tap, and can that number change? – jlstrecker Feb 24 '11 at 04:26
  • @jlstrecker no one would usually take more time than this for a double-tap. Just a random number I came up with after experimenting with a few. – lostInTransit Feb 28 '11 at 07:49
  • I think there is a tapCount = 0; missing in the first if clause, just before (or maybe after is more precise?) showing the alert – AndreaG May 17 '11 at 19:59
  • Why is this better than a single tap and double tap gesture recognizer? – ChuckKelly Mar 22 '16 at 10:37
  • @ChuckKelly its not. Gesture recognizers were introduced in 2010. This answer is from 2009, a year before they were made available. – lostInTransit Mar 28 '16 at 04:11
27

Override in your UITableView class this method

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
 {

     if(((UITouch *)[touches anyObject]).tapCount == 2)
    {
    NSLog(@"DOUBLE TOUCH");
    }
    [super touchesEnded:touches withEvent:event];
}
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
oxigen
  • 6,263
  • 3
  • 28
  • 37
  • Could you please explain me how do I exactly override the method? I created an object which inherits from UITableView and added a method you recommended. But this method doesn't get called. Thanks. – Ilya Suzdalnitski Jun 23 '09 at 15:50
  • 2
    Not to necropost per se, but this is exactly the sort of thing I was looking for - thanks! Only one gotcha for me. What if the UITableView in question is the one within the UITabBar's moreNavigationController? :( – Joe D'Andrea Sep 03 '09 at 20:21
  • 3
    You can also add this to your `UITableViewCell` class. – Kyle Fleming Jan 29 '13 at 22:35
10

In your UITableView subclass, do something like this:

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    for (UITouch* touch in touches) {
        if (touch.tapCount == 2)
        {
            CGPoint where = [touch locationInView:self];
            NSIndexPath* ip = [self indexPathForRowAtPoint:where];
            NSLog(@"double clicked index path: %@", ip);

            // do something useful with index path 'ip'
        }
    }

    [super touchesEnded:touches withEvent:event];
}
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
Matt Connolly
  • 9,757
  • 2
  • 65
  • 61
9

First define:

int tapCount;
NSIndexPath *tableSelection;

as class level variables in the .h file and do all the necessary setting up. Then...

- (void)tableView(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    tableSelection = indexPath;
    tapCount++;

    switch (tapCount) {
        case 1: //single tap
            [self performSelector:@selector(singleTap) withObject: nil afterDelay: .4];
            break;
        case 2: //double tap
            [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTap) object:nil];
            [self performSelector:@selector(doubleTap) withObject: nil];
            break;
        default:
            break;
    }
}

#pragma mark -
#pragma mark Table Tap/multiTap

- (void)singleTap {
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"Single tap detected" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
    [alert show];   
    tapCount = 0;
}

- (void)doubleTap {
    NSUInteger row = [tableSelection row];
    companyName = [self.suppliers objectAtIndex:row]; 
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert" message:@"DoubleTap" delegate:nil cancelButtonTitle:@"Yes" otherButtonTitles: nil];
    [alert show];
    tapCount = 0;
}
shim
  • 9,289
  • 12
  • 69
  • 108
Vladimir Stazhilov
  • 1,956
  • 4
  • 31
  • 63
  • 1
    you can simple `[self doubleTap];` instead of `[self performSelector:@selector(doubleTap) withObject: nil];` ;) – zxcat Feb 13 '13 at 14:44
  • 1
    This code has a few syntax errors, tried to edit but for some strange reason was rejected: http://stackoverflow.com/review/suggested-edits/2016633 – newenglander Apr 29 '13 at 13:44
6

I chose to implement it by overriding the UITableViewCell.

MyTableViewCell.h

@interface MyTableViewCell : UITableViewCell

@property (nonatomic, assign) int numberOfClicks;

@end

MyTableViewCell.m

@implementation MyTableViewCell

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
   UITouch *aTouch = [touches anyObject];
   self.numberOfClicks = [aTouch tapCount];
   [super touchesEnded:touches withEvent:event];
}

TableViewController.m

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

   MyTableViewCell *myCell = (MyTableViewCell*) [self.tableView cellForRowAtIndexPath:indexPath];

   NSLog(@"clicks:%d", myCell.numberOfClicks);

   if (myCell.numberOfClicks == 2) {
       NSLog(@"Double clicked");
   }
}
Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
masam
  • 2,268
  • 1
  • 22
  • 24
6
if([touch tapCount] == 1)
{
    [self performSelector:@selector(singleTapRecevied) withObject:self afterDelay:0.3];

} else if ([touch tapCount] == 2)
  {        
    [TapEnableImageView cancelPreviousPerformRequestsWithTarget:self selector:@selector(singleTapRecevied) object:self]; 
}

Use the performSelector to call a selector instead of using a timer. This solves the issue mentioned by @V1ru8.

WrightsCS
  • 50,551
  • 22
  • 134
  • 186
Fourj
  • 1,817
  • 1
  • 18
  • 34
3

Swift 3 solution from compare answers. No need any extensions, just add this code.

override func viewDidLoad() {
    viewDidLoad()

    let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleDoubleTap(sender:)))
    doubleTapGestureRecognizer.numberOfTapsRequired = 2
    tableView.addGestureRecognizer(doubleTapGestureRecognizer)

    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(sender:)))
    tapGestureRecognizer.numberOfTapsRequired = 1
    tapGestureRecognizer.require(toFail: doubleTapGestureRecognizer)
    tableView.addGestureRecognizer(tapGestureRecognizer)
}

func handleTapGesture(sender: UITapGestureRecognizer) {
    let touchPoint = sender.location(in: tableView)
    if let indexPath = tableView.indexPathForRow(at: touchPoint) {
        print(indexPath)
    }
}

func handleDoubleTap(sender: UITapGestureRecognizer) {
    let touchPoint = sender.location(in: tableView)
    if let indexPath = tableView.indexPathForRow(at: touchPoint) {
        print(indexPath)
    }
}
Kamil Harasimowicz
  • 4,684
  • 5
  • 32
  • 58
  • Would you have to put any delay into the handleTapGesture method? I feel like handleTapGesture would execute before a second tap is detected and so handleTapGesture wouldn't have any time to fail. I'm kind of a noob though, so please explain if I'm not getting it correctly. – Sam Dec 14 '19 at 01:38
  • `require(toFail:)` makes it work https://developer.apple.com/documentation/uikit/uigesturerecognizer/1624203-require – Kamil Harasimowicz Dec 14 '19 at 08:52
2

Another answer

int touches;

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  touches++;

    if(touches==2){
       //your action
    }
}

- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
    touches=0;
}
Cyklet
  • 351
  • 2
  • 12
1

You'll probably need to subclass UITableView and override whatever touch events are appropriate (touchesBegan:withEvent;, touchesEnded:withEvent, etc.) Inspect the events to see how many touches there were, and do your custom behavior. Don't forget to call through to UITableView's touch methods, or else you won't get the default behavior.

Paras Joshi
  • 20,427
  • 11
  • 57
  • 70
zpasternack
  • 17,838
  • 2
  • 63
  • 81
1

According to @lostInTransit I prepared code in Swift

var tapCount:Int = 0
var tapTimer:NSTimer?
var tappedRow:Int?

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    //checking for double taps here
    if(tapCount == 1 && tapTimer != nil && tappedRow == indexPath.row){
        //double tap - Put your double tap code here
        tapTimer?.invalidate()
        tapTimer = nil
    }
    else if(tapCount == 0){
        //This is the first tap. If there is no tap till tapTimer is fired, it is a single tap
        tapCount = tapCount + 1;
        tappedRow = indexPath.row;
        tapTimer = NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: "tapTimerFired:", userInfo: nil, repeats: false)
    }
    else if(tappedRow != indexPath.row){
        //tap on new row
        tapCount = 0;
        if(tapTimer != nil){
            tapTimer?.invalidate()
            tapTimer = nil
        }
    }
}

func tapTimerFired(aTimer:NSTimer){
//timer fired, there was a single tap on indexPath.row = tappedRow
    if(tapTimer != nil){
        tapCount = 0;
        tappedRow = -1;
    }
}
Roman Barzyczak
  • 3,785
  • 1
  • 30
  • 44
0

Note: please see the comments below to see though while this solution worked for me, it still may not be a good idea.

An alternative to creating a subclass of UITableView or UITableViewCell (and to using a timer) would be just to extend the UITableViewCell class with a category, for example (using @oxigen's answer, in this case for the cell instead of the table):

@implementation UITableViewCell (DoubleTap)
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if(((UITouch *)[touches anyObject]).tapCount == 2)
    {
        NSLog(@"DOUBLE TOUCH");
    }
    [super touchesEnded:touches withEvent:event];
}
@end

This way you don't have to go around renaming existing instances of UITableViewCell with the new class name (will extend all instances of the class).

Note that now super in this case (this being a category) doesn't refer to UITableView but to its super, UITView. But the actual method call to touchesEnded:withEvent: is in UIResponder (of which both UITView and UITableViewCell are subclasses), so there's no difference there.

newenglander
  • 2,019
  • 24
  • 55
0

Here's my complete solution:

CustomTableView.h

//
//  CustomTableView.h
//

#import <UIKit/UIKit.h>

@interface CustomTableView : UITableView

    // Nothing needed here

@end

CustomTableView.m

//
//  CustomTableView.m
//

#import "CustomTableView.h"

@implementation CustomTableView


//
// Touch event ended
//
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{

    // For each event received
    for (UITouch * touch in touches) {

        NSIndexPath * indexPath = [self indexPathForRowAtPoint: [touch locationInView:self] ];

        // One tap happened
        if([touch tapCount] == 1)
        {
            // Call the single tap method after a delay
            [self performSelector: @selector(singleTapReceived:)
                       withObject: indexPath
                       afterDelay: 0.3];
        }


        // Two taps happened
        else if ([touch tapCount] == 2)
        {
            // Cancel the delayed call to the single tap method
            [NSObject cancelPreviousPerformRequestsWithTarget: self
                                                     selector: @selector(singleTapReceived:)
                                                       object: indexPath ];

            // Call the double tap method instead
            [self performSelector: @selector(doubleTapReceived:)
                       withObject: indexPath ];
        }


    }

    // Pass the event to super
    [super touchesEnded: touches
              withEvent: event];

}


//
// Single Tap
//
-(void) singleTapReceived:(NSIndexPath *) indexPath
{
    NSLog(@"singleTapReceived - row: %ld",(long)indexPath.row);
}


//
// Double Tap
//
-(void) doubleTapReceived:(NSIndexPath *) indexPath
{
    NSLog(@"doubleTapReceived - row: %ld",(long)indexPath.row);
}



@end
grigb
  • 1,151
  • 8
  • 14
0

Improvement for oxigen answer.

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    UITouch *touch = [touches anyObject];
    if(touch.tapCount == 2) {
        CGPoint touchPoint = [touch locationInView:self];
        NSIndexPath *touchIndex = [self indexPathForRowAtPoint:touchPoint];
        if (touchIndex) {
            // Call some callback function and pass 'touchIndex'.
        }
    }
    [super touchesEnded:touches withEvent:event];
}
Community
  • 1
  • 1
user1763487
  • 464
  • 4
  • 6
0

This approach has been tested to produce consistent results.

Lets keep it simple and straightforward. A UITapGestureRecognizer on your UITableView with numberOfTapsRequired set to 2 does the job efficiently and without interrupting the regular taps using cell selection with didSelectRow.

So, we have the recogniser creation:

let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(tableViewCellDoubleTapped(_:)))
gestureRecognizer.numberOfTapsRequired = 2
tableView.addGestureRecognizer(gestureRecognizer)

and then we have the action:

@objc func tableViewCellDoubleTapped(_ sender: UITapGestureRecognizer) {
    let touchPoint = sender.location(in: sender.view)
    if let indexPath = tableView.indexPathForRow(at: touchPoint) {
        // Profit!
    }
}
halfer
  • 19,824
  • 17
  • 99
  • 186
D6mi
  • 611
  • 7
  • 16
0

You can make and easy setup by using below code, this is the simplest logic for detect a double tap on a certain cell in UITableView.

var isDouble = false
var myTimer = Timer()
var secondsToCount = 0.0

@objc func updateTimer() {
    secondsToCount += 0.1
    isDouble = true
    if secondsToCount >= 0.5 {
        myTimer.invalidate()
        secondsToCount = 0.0
        
        isDouble = false
    }
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    myTimer.invalidate()
    myTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTimer), userInfo: ["index":indexPath.row], repeats: true)
    
    if isDouble == true{
        //you can make logic for double click action here.
        print("DOUBLE CLICK EVENT FIRED in 0.5 Seconds ")

    }else{
        // this will not in use.
    }
}
-1

This solution is only works for UICollectionView or UITableView's cell.

First declare these variables

int number_of_clicks;

BOOL thread_started;

Then put this code in your didSelectItemAtIndexPath

++number_of_clicks;
if (!thread_started) {

    thread_started = YES;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                 0.25 * NSEC_PER_SEC),
                   dispatch_get_main_queue(),^{

                       if (number_of_clicks == 1) {
                           ATLog(@"ONE");
                       }else if(number_of_clicks == 2){
                           ATLog(@"DOUBLE");
                       }

                       number_of_clicks = 0;
                       thread_started = NO;

                   });

        }

0.25 is the delay of between 2 clicks. I think 0.25 is perfect for detect this type of click. Now you can detect only one click and two clicks seperately. Good Luck

eQmn
  • 1
  • 4