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

16

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.

Yordan answered 23/6, 2009 at 8:9 Comment(3)
you mean a double tap or a multi-touch?Robber
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.Subgenus
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.Marduk
R
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

Robber answered 23/6, 2009 at 13:11 Comment(6)
Would you mind posting more of your code. Where are you setting the tapTimer, tappedRow, and tapCount? Thanks!Excrescent
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?Vantage
@Vantage 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.Robber
I think there is a tapCount = 0; missing in the first if clause, just before (or maybe after is more precise?) showing the alertSyd
Why is this better than a single tap and double tap gesture recognizer?Ettie
@Ettie its not. Gesture recognizers were introduced in 2010. This answer is from 2009, a year before they were made available.Robber
S
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];
}
Shatterproof answered 23/6, 2009 at 10:21 Comment(3)
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.Yordan
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? :(Bradfield
You can also add this to your UITableViewCell class.Khania
M
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];
}
Mani answered 22/7, 2010 at 2:18 Comment(0)
R
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;
}
Reese answered 12/9, 2011 at 15:54 Comment(2)
you can simple [self doubleTap]; instead of [self performSelector:@selector(doubleTap) withObject: nil]; ;)Spitz
This code has a few syntax errors, tried to edit but for some strange reason was rejected: stackoverflow.com/review/suggested-edits/2016633Banks
C
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.

Colorcast answered 4/5, 2011 at 8:39 Comment(0)
J
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");
   }
}
Jonijonie answered 23/1, 2013 at 8:43 Comment(0)
O
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)
    }
}
Outcome answered 20/1, 2017 at 14:19 Comment(2)
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.Haffner
require(toFail:) makes it work developer.apple.com/documentation/uikit/uigesturerecognizer/…Outcome
A
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.

Aspa answered 23/6, 2009 at 8:51 Comment(0)
U
1

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;
}
Undercarriage answered 18/9, 2014 at 8:56 Comment(1)
best and shortest answer ever!Eli
A
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;
    }
}
Antabuse answered 14/6, 2015 at 13:13 Comment(0)
B
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.

Banks answered 29/4, 2013 at 13:4 Comment(9)
Overwriting an existing method in a category may or may not work.Crusado
#5272951Crusado
The original UITableViewCell isn't a category, so I don't see how this would have an effect here. But I've updated the last part of my answer to make it more clear.Banks
How do you know that apple didn't break up the implementation in several categories?Crusado
when you write a category u also define it in your own file. So are you checking every header file before using a class? and even if a method is not defined in a category: it could be changed in any future version. Just never use categories for overriding.Crusado
Ok that makes sense. I guess I'll delete this answer. Thanks for not marking down.Banks
it is technical ok and should compile — but in this case this ist just not the full picture. not a reason to vote down IMO.Crusado
It worked for me in a brief test on one device, but I ended up using the solutions with timers myself.Banks
I guess I would use a UITapGestureRecognizerCrusado
S
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
Synapsis answered 13/3, 2014 at 0:43 Comment(0)
D
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];
}
Dm answered 29/4, 2014 at 9:56 Comment(0)
O
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!
    }
}
Openfaced answered 11/1, 2022 at 11:22 Comment(0)
P
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.
    }
}
Partook answered 22/9, 2022 at 12:25 Comment(0)
U
-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

Ultramicrometer answered 2/11, 2017 at 1:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.