196

I would like to handle a long press on a UITableViewCell to print a "quick access menu". Did someone already do this?

Particularly the gesture recognize on UITableView?

TheNeil
  • 3,321
  • 2
  • 27
  • 52
foOg
  • 3,126
  • 3
  • 19
  • 18

10 Answers10

435

First add the long press gesture recognizer to the table view:

UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc] 
  initWithTarget:self action:@selector(handleLongPress:)];
lpgr.minimumPressDuration = 2.0; //seconds
lpgr.delegate = self;
[self.myTableView addGestureRecognizer:lpgr];
[lpgr release];

Then in the gesture handler:

-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
    CGPoint p = [gestureRecognizer locationInView:self.myTableView];

    NSIndexPath *indexPath = [self.myTableView indexPathForRowAtPoint:p];
    if (indexPath == nil) {
        NSLog(@"long press on table view but not on a row");
    } else if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
        NSLog(@"long press on table view at row %ld", indexPath.row);
    } else {
        NSLog(@"gestureRecognizer.state = %ld", gestureRecognizer.state);
    }
}

You have to be careful with this so that it doesn't interfere with the user's normal tapping of the cell and also note that handleLongPress may fire multiple times (this will be due to the gesture recognizer state changes).

CDspace
  • 2,639
  • 18
  • 30
  • 36
  • 2
    Awesome !!! Thanks a lot! But a last little question: Why is the method handleLongPress is call when the touch ended ??? – foOg Oct 13 '10 at 15:15
  • It's not called when touch ends but can fire multiple times if the user keeps their finger on the cell for more than 4 seconds (in this example) before lifting it. –  Oct 13 '10 at 15:19
  • 113
    Correction: it fires multiple times to indicate the different states of the gesture (began, changed, ended, etc). So in the handler method, check the state property of the gesture recognizer to avoid doing the action at each state of the gesture. Eg: `if (gestureRecognizer.state == UIGestureRecognizerStateBegan) ...`. –  Oct 18 '10 at 15:12
  • tnx Anna Karenina,your answer helped me to deliver my task in time and moreever i tried in a different approach before seeing your code. now i solved it easily upon looking yours – Dinakar Jun 22 '11 at 08:48
  • it`s nice code but when i implement this in my application handlermethod called twice... m not getting this. if you have any idea anna than tel me. Thanks. – AJPatel Sep 07 '11 at 10:06
  • @AJPatel, look at the "correction" comment above. Need to check gestureRecognizer.state. –  Sep 07 '11 at 10:56
  • 4
    Don't forget, gesture recognizers can now be added to UI elements directly in Interface Builder and connected through an IBAction method, so this answer is even easier ;-) (just remember to attach the recognizer to the `UITableView`, not the `UITableViewCell`...) –  Jun 26 '12 at 15:51
  • 11
    Also confirm to UIGestureRecognizerDelegate protocol in class.h file – jay Aug 09 '12 at 05:30
  • hi i am following the same thing, on long press i am inserting a uiview over tableview. the thing that is going wrong is that while releasing the long press, "UIGestureRecognizerStateEnded" is not recognised. anything suggested? – Suhaiyl Mar 21 '13 at 13:23
  • This worked more reliably for me after I also added `lpgr.allowableMovement` – Roger Mar 02 '16 at 01:59
  • 2
    How do you prevent the table view from navigating into the cell (if you have 'didSelectRowAtIndexPath' implemented?) – Marchy Sep 15 '16 at 17:45
  • 1
    What about `didSelect` is already there ? how to prevent if long press is executing or cell just tapped ? – Prashant Tukadiya Feb 14 '18 at 12:33
  • how to show a "delete" button where ever i press long with black background as like when we hold cell for long and option come in just above cell copy,delete,cut as like deleting message in whatsapp on long hold @Anna – Dilip Tiwari Jul 21 '18 at 04:25
  • For me, it is giving indexPath of row above tapped row, when long tapped point is slightly above the middle of tapped row. – Shivam Pokhriyal Sep 24 '18 at 19:49
47

I've used Anna-Karenina's answer, and it works almost great with a very serious bug.

If you're using sections, long-pressing the section title will give you a wrong result of pressing the first row on that section, I've added a fixed version below (including the filtering of dummy calls based on the gesture state, per Anna-Karenina suggestion).

- (IBAction)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {

        CGPoint p = [gestureRecognizer locationInView:self.tableView];

        NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
        if (indexPath == nil) {
            NSLog(@"long press on table view but not on a row");
        } else {
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            if (cell.isHighlighted) {
                NSLog(@"long press on table view at section %d row %d", indexPath.section, indexPath.row);
            }
        }
    }
}
marmor
  • 27,641
  • 11
  • 107
  • 150
  • Hi @marmor: I want to ask that is it possible to identify only a portion of a view which user touched? – Manthan Mar 10 '14 at 06:16
  • Hey, you can use hitTest for that (https://developer.apple.com/library/ios/documentation/uikit/reference/uiview_class/uiview/uiview.html#//apple_ref/occ/instm/UIView/hitTest:withEvent:). Check this answer for example on how to use: http://stackoverflow.com/a/2793253/819355 – marmor Mar 10 '14 at 10:53
  • While the accepted answer works. It does log multiple fires, along with logging while dragging on the screen. This answer does not. A legit copy and paste answer. Thank You. – ChrisOSX Mar 04 '17 at 09:53
40

Answer in Swift 5 (Continuation of Ricky's answer in Swift)

Add the UIGestureRecognizerDelegate to your ViewController

 override func viewDidLoad() {
    super.viewDidLoad()

    //Long Press
    let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
    longPressGesture.minimumPressDuration = 0.5
    self.tableView.addGestureRecognizer(longPressGesture)
 }

And the function:

@objc func handleLongPress(longPressGesture: UILongPressGestureRecognizer) {
    let p = longPressGesture.location(in: self.tableView)
    let indexPath = self.tableView.indexPathForRow(at: p)
    if indexPath == nil {
        print("Long press on table view, not row.")
    } else if longPressGesture.state == UIGestureRecognizer.State.began {
        print("Long press on row, at \(indexPath!.row)")
    }
}
lewis
  • 2,936
  • 2
  • 37
  • 72
Ben
  • 576
  • 6
  • 4
22

Here are clarified instruction combining Dawn Song's answer and Marmor's answer.

Drag a long Press Gesture Recognizer and drop it into your Table Cell. It will jump to the bottom of the list on the left.

enter image description here

Then connect the gesture recognizer the same way you would connect a button. enter image description here

Add the code from Marmor in the the action handler

- (IBAction)handleLongPress:(UILongPressGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {

    CGPoint p = [sender locationInView:self.tableView];

    NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:p];
    if (indexPath == nil) {
        NSLog(@"long press on table view but not on a row");
    } else {
        UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
        if (cell.isHighlighted) {
            NSLog(@"long press on table view at section %d row %d", indexPath.section, indexPath.row);
        }
    }
}

}

Ryan Heitner
  • 13,119
  • 6
  • 77
  • 119
15

Answer in Swift:

Add delegate UIGestureRecognizerDelegate to your UITableViewController.

Within UITableViewController:

override func viewDidLoad() {
    super.viewDidLoad()

    let longPressGesture:UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
    longPressGesture.minimumPressDuration = 1.0 // 1 second press
    longPressGesture.delegate = self
    self.tableView.addGestureRecognizer(longPressGesture)

}

And the function:

func handleLongPress(longPressGesture:UILongPressGestureRecognizer) {

    let p = longPressGesture.locationInView(self.tableView)
    let indexPath = self.tableView.indexPathForRowAtPoint(p)

    if indexPath == nil {
        print("Long press on table view, not row.")
    }
    else if (longPressGesture.state == UIGestureRecognizerState.Began) {
        print("Long press on row, at \(indexPath!.row)")
    }

}
Ricky
  • 7,785
  • 2
  • 34
  • 46
15

Looks to be more efficient to add the recognizer directly to the cell as shown here:

Tap&Hold for TableView Cells, Then and Now

(scroll to the example at the bottom)

Baby Groot
  • 4,637
  • 39
  • 52
  • 71
J.R.
  • 199
  • 1
  • 2
  • 7
    How can allocating a new gesture recognizer object for each row be more efficient than a single recognizer for the whole table? – user2393462435 Dec 23 '11 at 17:50
  • 8
    Remember though there is only a few cells created if dequeue is working correctly. – Ants Feb 07 '12 at 22:55
7

I put together a little category on UITableView based on Anna Karenina's excellent answer.

Like this you'll have a convenient delegate method like you're used to when dealing with regular table views. Check it out:

//  UITableView+LongPress.h

#import <UIKit/UIKit.h>

@protocol UITableViewDelegateLongPress;

@interface UITableView (LongPress) <UIGestureRecognizerDelegate>
@property(nonatomic,assign)   id <UITableViewDelegateLongPress>   delegate;
- (void)addLongPressRecognizer;
@end


@protocol UITableViewDelegateLongPress <UITableViewDelegate>
- (void)tableView:(UITableView *)tableView didRecognizeLongPressOnRowAtIndexPath:(NSIndexPath *)indexPath;
@end



//  UITableView+LongPress.m

#import "UITableView+LongPress.h"

@implementation UITableView (LongPress)
@dynamic delegate;

- (void)addLongPressRecognizer {
    UILongPressGestureRecognizer *lpgr = [[UILongPressGestureRecognizer alloc]
                                          initWithTarget:self action:@selector(handleLongPress:)];
    lpgr.minimumPressDuration = 1.2; //seconds
    lpgr.delegate = self;
    [self addGestureRecognizer:lpgr];
}


- (void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
    CGPoint p = [gestureRecognizer locationInView:self];

    NSIndexPath *indexPath = [self indexPathForRowAtPoint:p];
    if (indexPath == nil) {
        NSLog(@"long press on table view but not on a row");
    }
    else {
        if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
            // I am not sure why I need to cast here. But it seems to be alright.
            [(id<UITableViewDelegateLongPress>)self.delegate tableView:self didRecognizeLongPressOnRowAtIndexPath:indexPath];
        }
    }
}

If you want to use this in a UITableViewController, you probably need to subclass and conform to the new protocol.

It works great for me, hope it helps others!

de.
  • 7,068
  • 3
  • 40
  • 69
6

Swift 3 answer, using modern syntax, incorporating other answers, and eliminating unneeded code.

override func viewDidLoad() {
    super.viewDidLoad()
    let recognizer = UILongPressGestureRecognizer(target: self, action: #selector(tablePressed))
    tableView.addGestureRecognizer(recognizer)
 }

@IBAction func tablePressed(_ recognizer: UILongPressGestureRecognizer) {
    let point = recognizer.location(in: tableView)

    guard recognizer.state == .began,
          let indexPath = tableView.indexPathForRow(at: point),
          let cell = tableView.cellForRow(at: indexPath),
          cell.isHighlighted
    else {
        return
    }

    // TODO
}
phatmann
  • 18,161
  • 7
  • 61
  • 51
3

Just add UILongPressGestureRecognizer to the given prototype cell in storyboard, then pull the gesture to the viewController's .m file to create an action method. I made it as I said.

DawnSong
  • 4,752
  • 2
  • 38
  • 38
-2

Use the UITouch timestamp property in touchesBegan to launch a timer or stop it when touchesEnded got fired

Thomas Joulin
  • 6,590
  • 9
  • 53
  • 88
  • Thanks for your answer but how can i detect which row is concerned by the touch ? – foOg Oct 13 '10 at 14:05
  • I might be wrong, but nothing is provided to help you do that. You'll have to get the indexes of the current visible cells with [tableView indexPathsForVisibleRows] and then, using some calculations (your tableView offset from the top + X times the rows) you'll know that the coordinates of your finger is on which row. – Thomas Joulin Oct 13 '10 at 14:17
  • I'm sure that there is an easier way to do that, anyway if you have another idea, i'll be here :) – foOg Oct 13 '10 at 14:32
  • I'd be glad to know too if something easier is possible. But I don't think there is, mostly because the is not the way Apple wants us to handle interactions... It looks like an Android way of thinking this "quick access menu". If it were my app, I'll handle it like the Twitter app. A swipe to the left displays the options – Thomas Joulin Oct 13 '10 at 14:37
  • Yes, i thought about that, so if i really can't do it with a long press event, i'll use the swipe method. But, maybe someone in stack-overflow did it ... – foOg Oct 13 '10 at 14:47