110

I was wondering how to add a long press gesture recognizer to a (subclass of) UICollectionView. I read in the documentation that it is added by default, but I can't figure out how.

What I want to do is: Long press on a cell ( I have a calendar thingy from github ), get which cell is tapped and then do stuff with it. I need to know what cell is longpressed. Sorry for this broad question, but i couldn't find anything better on either google or SO

Oscar Apeland
  • 5,952
  • 5
  • 39
  • 85

9 Answers9

223

Objective-C

In your myCollectionViewController.h file add the UIGestureRecognizerDelegate protocol

@interface myCollectionViewController : UICollectionViewController<UIGestureRecognizerDelegate>

in your myCollectionViewController.m file:

- (void)viewDidLoad
{
    // attach long press gesture to collectionView
    UILongPressGestureRecognizer *lpgr 
       = [[UILongPressGestureRecognizer alloc]
                     initWithTarget:self action:@selector(handleLongPress:)];
    lpgr.delegate = self;
    lpgr.delaysTouchesBegan = YES;
    [self.collectionView addGestureRecognizer:lpgr];
}

-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
    if (gestureRecognizer.state != UIGestureRecognizerStateEnded) {
        return;
    }
    CGPoint p = [gestureRecognizer locationInView:self.collectionView];

    NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:p];
    if (indexPath == nil){
        NSLog(@"couldn't find index path");            
    } else {
        // get the cell at indexPath (the one you long pressed)
        UICollectionViewCell* cell =
        [self.collectionView cellForItemAtIndexPath:indexPath];
        // do stuff with the cell
    }
}

Swift

class Some {

    @objc func handleLongPress(gesture : UILongPressGestureRecognizer!) {
        if gesture.state != .Ended {
            return
        }
        let p = gesture.locationInView(self.collectionView)

        if let indexPath = self.collectionView.indexPathForItemAtPoint(p) {
            // get the cell at indexPath (the one you long pressed)
            let cell = self.collectionView.cellForItemAtIndexPath(indexPath)
            // do stuff with the cell
        } else {
            print("couldn't find index path")
        }
    }
}

let some = Some()
let lpgr = UILongPressGestureRecognizer(target: some, action: #selector(Some.handleLongPress))

Swift 4

class Some {

    @objc func handleLongPress(gesture : UILongPressGestureRecognizer!) {
        if gesture.state != .ended { 
            return 
        } 

        let p = gesture.location(in: self.collectionView) 

        if let indexPath = self.collectionView.indexPathForItem(at: p) { 
            // get the cell at indexPath (the one you long pressed) 
            let cell = self.collectionView.cellForItem(at: indexPath) 
            // do stuff with the cell 
        } else { 
            print("couldn't find index path") 
        }
    }
}

let some = Some()
let lpgr = UILongPressGestureRecognizer(target: some, action: #selector(Some.handleLongPress))
JonSlowCN
  • 308
  • 3
  • 11
abbood
  • 21,507
  • 9
  • 112
  • 218
  • Bonus noob question: How do I get a cell from indexpath? It worked btw, thanks – Oscar Apeland Sep 17 '13 at 11:44
  • 1
    it's already in the answer : `UICollectionViewCell* cell = [self.collectionView cellForItemAtIndexPath:indexPath];` reference [here](https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionView_class/Reference/Reference.html#//apple_ref/occ/instm/UICollectionView/cellForItemAtIndexPath:) hope all this merits a correct answer award :D – abbood Sep 17 '13 at 11:46
  • @abbood how can the buildin longpress gesture recognizer not interfere with the custom one in ur code? I think that was the real question here – tiguero Sep 17 '13 at 11:55
  • @tiguero what built-in longpress gesture recognizer are you talking about? – abbood Sep 17 '13 at 12:01
  • 10
    For (at least) ios7 you have to add `lpgr.delaysTouchesBegan = YES;` to avoid the `didHighlightItemAtIndexPath` being triggered first. – DynamicDan Jan 01 '14 at 17:29
  • @DynamicDan thanks! I'll update my answer to include your comment – abbood Jan 02 '14 at 05:39
  • 7
    Why did you add `lpgr.delegate = self;`? It works fine without delegate, which you have not provided, either. – Yevhen Dubinin Aug 07 '14 at 11:06
  • 3
    @abbood the answer works, but i can not scroll up and down in the collectionview (using another finger) while the long press recognizer is active. What gives? – Pétur Ingi Egilsson Aug 18 '14 at 09:46
  • 4
    Personally, I would do `UIGestureRecognizerStateBegan`, so the gesture is used when it is recognized, not when the user releases their finger. – Jeffrey Sun Apr 18 '15 at 19:50
  • 1
    @abbood @DynamicDan, I don't need to add the `lpgr.delaysTouchesBegan = YES` line when programming for iOS 9 in Swift. Has something changed? – Loic Verrall Aug 11 '15 at 22:42
  • @abbood how can I exchange position of cells on which we swipe? I am trying to exchange position of cells by gesture. – Bhavin Ramani Dec 28 '15 at 09:52
  • @bhavinramani that's a separate question.. please create a new one and link to it from the comments – abbood Dec 28 '15 at 09:55
  • @abbood http://stackoverflow.com/questions/34491561/exchange-position-of-cells-by-gesture – Bhavin Ramani Dec 28 '15 at 10:15
  • @abbood please give me solution for above link. – Bhavin Ramani Dec 29 '15 at 04:44
  • Have you tried the answer posted on your question? The answerer works on my team he's good – abbood Dec 29 '15 at 04:47
  • @abbood thanks a lot.. I added comment on that answer please take a look. Lobo Labs rockss.... – Bhavin Ramani Dec 29 '15 at 05:19
  • lpgr.delegate = self; lpgr.delaysTouchesBegan = YES; is redundant – jk2K Jan 29 '16 at 07:47
  • Update for Swfit 4: if gesture.state != .ended { return } let p = gesture.location(in: self.collectionView) if let indexPath = self.collectionView.indexPathForItem(at: p) { // get the cell at indexPath (the one you long pressed) let cell = self.collectionView.cellForItem(at: indexPath) // do stuff with the cell } else { print("couldn't find index path") } – JonSlowCN Dec 06 '17 at 06:48
  • @YevhenDubinin , In my case, I need to add `lpgr.delegate = self;` line, then handleLongPress function got call. iOS 12.2 Simulator with Version 11.3 (11C29) – Jerome May 23 '20 at 03:09
31

The same code @abbood's code for Swift:

In viewDidLoad:

let lpgr : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleLongPress:")
lpgr.minimumPressDuration = 0.5
lpgr.delegate = self
lpgr.delaysTouchesBegan = true
self.collectionView?.addGestureRecognizer(lpgr)

And the function:

func handleLongPress(gestureRecognizer : UILongPressGestureRecognizer){

    if (gestureRecognizer.state != UIGestureRecognizerState.Ended){
        return
    }

    let p = gestureRecognizer.locationInView(self.collectionView)

    if let indexPath : NSIndexPath = (self.collectionView?.indexPathForItemAtPoint(p))!{
        //do whatever you need to do
    }

}

Do not forget the delegate UIGestureRecognizerDelegate

  • 3
    Worked great, just a note that "handleLongPress:" should be changed to #selector(YourViewController.handleLongPress(_:)) – Joseph Geraghty Apr 18 '16 at 17:20
  • 16
    Works well, but change `UIGestureRecognizerState.Ended` to `UIGestureRecognizerState.Began` if you want the code to fire once the minimum duration has passed, not just when the user picks up his/her finger. – Crashalot May 01 '16 at 22:59
  • self.myCollectionView?.indexPathForItem(at: p) returns Fatal error: Unexpectedly found nil while unwrapping an Optional value when clicked anywhere else apart from cells in the collectionview – Arjun Oct 27 '20 at 09:03
  • This method throws Fatal error: Unexpectedly found nil while unwrapping an Optional value error when you click on CollectionView and if no cell is there. How to resolve this error? – Arjun Oct 27 '20 at 09:14
11

Use the delegate of UICollectionView receive long press event

You must impl 3 method below.

//UICollectionView menu delegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath{

   //Do something

   return YES;
}
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender{
    //do nothing
    return NO;
}

- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender{
    //do nothing
}
liuyuning
  • 141
  • 1
  • 5
  • Note: If you return false for shouldShowMenuForItemAtIndexPath, the didSelectItemAtIndexPath will also be kicked off as well. This became problematic for me when I wanted two different actions for long press vs single press. – ShannonS May 26 '17 at 21:57
  • it is deprecated methods for now, you can use - (UIContextMenuConfiguration *)collectionView:(UICollectionView *)collectionView contextMenuConfigurationForItemAtIndexPath:(nonnull NSIndexPath *)indexPath point:(CGPoint)point; – Viktor Goltvyanitsa Mar 08 '20 at 11:49
10

Swift 5:

private func setupLongGestureRecognizerOnCollection() {
    let longPressedGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(gestureRecognizer:)))
    longPressedGesture.minimumPressDuration = 0.5
    longPressedGesture.delegate = self
    longPressedGesture.delaysTouchesBegan = true
    collectionView?.addGestureRecognizer(longPressedGesture)
}

@objc func handleLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
    if (gestureRecognizer.state != .began) {
        return
    }

    let p = gestureRecognizer.location(in: collectionView)

    if let indexPath = collectionView?.indexPathForItem(at: p) {
        print("Long press at item: \(indexPath.row)")
    }
}

Also don't forget to implement UIGestureRecognizerDelegate and call setupLongGestureRecognizerOnCollection from viewDidLoad or wherever you need to call it.

Renexandro
  • 374
  • 4
  • 13
  • This method throws Fatal error: Unexpectedly found nil while unwrapping an Optional value error when you click on CollectionView and if no cell is there. How to resolve this error? – Arjun Oct 27 '20 at 09:15
  • why u have to set delegate when u did not implement any this method? – famfamfam Apr 19 '21 at 10:12
8

Answers here to add a custom longpress gesture recognizer are correct however according to the documentation here: the parent class of UICollectionView class installs a default long-press gesture recognizer to handle scrolling interactions so you must link your custom tap gesture recognizer to the default recognizer associated with your collection view.

The following code will avoid your custom gesture recognizer to interfere with the default one:

UILongPressGestureRecognizer* longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressGesture:)];

longPressGesture.minimumPressDuration = .5; //seconds
longPressGesture.delegate = self;

// Make the default gesture recognizer wait until the custom one fails.
for (UIGestureRecognizer* aRecognizer in [self.collectionView gestureRecognizers]) {
   if ([aRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
      [aRecognizer requireGestureRecognizerToFail:longPressGesture];
} 
Crashalot
  • 31,452
  • 56
  • 235
  • 393
tiguero
  • 11,171
  • 5
  • 41
  • 61
  • i see what you're saying, but it's not black and white, documentation says: `The parent class of UICollectionView class installs a default tap gesture recognizer and a default long-press gesture recognizer to handle scrolling interactions. You should never try to reconfigure these default gesture recognizers or replace them with your own versions.` so the default long-press recognizer is made for scrolling.. which implies it has to be accompanied with a vertical movement.. the OP isn't asking about that kind of behavior nor is he trying to replace it – abbood Sep 17 '13 at 12:22
  • sorry for sounding defensive, but I've been using the [above](http://stackoverflow.com/a/18848817/766570) code with my iOS app for months.. can't think of a single time a glitch happened – abbood Sep 17 '13 at 12:29
  • 1
    @abbood that's fine. I don't know the third-party calendar component the PO is using but i think preventing a glitch even if u think it can never happen couldn't be a bad idea ;-) – tiguero Sep 17 '13 at 12:32
  • If you must wait for the default recognizer to fail doesn't that mean there will be a delay? – Crashalot May 01 '16 at 23:01
2
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];

[cell addGestureRecognizer:longPress];

and add the method like this.

- (void)longPress:(UILongPressGestureRecognizer*)gesture
{
    if ( gesture.state == UIGestureRecognizerStateEnded ) {

        UICollectionViewCell *cellLongPressed = (UICollectionViewCell *) gesture.view;
    }
}
Satheesh
  • 10,292
  • 6
  • 47
  • 89
2

To have an external gesture recognizer and do not conflict with internal gesture recognizers on the UICollectionView you need to:

Add your gesture recognizer, set up it and capture a reference for it somewhere (the best option is on your subclass if you subclassed UICollectionView)

@interface UICollectionViewSubclass : UICollectionView <UIGestureRecognizerDelegate>    

@property (strong, nonatomic, readonly) UILongPressGestureRecognizer *longPressGestureRecognizer;   

@end

Override default initialization methods initWithFrame:collectionViewLayout: and initWithCoder: and add set up method for you long press gesture recognizer

@implementation UICollectionViewSubclass

-(instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout
{
    if (self = [super initWithFrame:frame collectionViewLayout:layout]) {
        [self setupLongPressGestureRecognizer];
    }
    return self;
}

-(instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder]) {
        [self setupLongPressGestureRecognizer];
    }
    return self;
}

@end

Write your setup method so it instantiates long press gesture recognizer, set it's delegate, setup dependencies with UICollectionView gesture recognizer (so it be the main gesture and all other gestures will wait till that gesture fails before being recognized) and add gesture to the view

-(void)setupLongPressGestureRecognizer
{
    _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
                                                                                action:@selector(handleLongPressGesture:)];
    _longPressGestureRecognizer.delegate = self;

    for (UIGestureRecognizer *gestureRecognizer in self.collectionView.gestureRecognizers) {
        if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
            [gestureRecognizer requireGestureRecognizerToFail:_longPressGestureRecognizer];
        }
    }

    [self.collectionView addGestureRecognizer:_longPressGestureRecognizer];
}

Also don't forget to implement UIGestureRecognizerDelegate methods that fails that gesture and allow simultaneous recognition (you may or may don't need to implement it, it depends on other gesture recognizers you have or dependencies with internal gesture recognizers)

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    if ([self.longPressGestureRecognizer isEqual:gestureRecognizer]) {
        return NO;
    }

    return NO;
}

credentials for that goes to internal implementation of LXReorderableCollectionViewFlowLayout

0

Perhaps, using UILongPressGestureRecognizer is the most widespread solution. But I encounter with it two annoying troubles:

  • sometimes this recognizer works in incorrect way when we're moving our touch;
  • recognizer intercepts other touch actions so we can't use highlight callbacks of our UICollectionView in a proper way.

Let me suggest one a little bit bruteforce, but working as it's required suggestion:

Declaring a callback description for long click on our cell:

typealias OnLongClickListener = (view: OurCellView) -> Void

Extending UICollectionViewCell with variables (we can name it OurCellView, for example):

/// To catch long click events.
private var longClickListener: OnLongClickListener?

/// To check if we are holding button pressed long enough.
var longClickTimer: NSTimer?

/// Time duration to trigger long click listener.
private let longClickTriggerDuration = 0.5

Adding two methods in our cell class:

/**
 Sets optional callback to notify about long click.

 - Parameter listener: A callback itself.
 */
func setOnLongClickListener(listener: OnLongClickListener) {
    self.longClickListener = listener
}

/**
 Getting here when long click timer finishs normally.
 */
@objc func longClickPerformed() {
    self.longClickListener?(view: self)
}

And overriding touch events here:

/// Intercepts touch began action.
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    longClickTimer = NSTimer.scheduledTimerWithTimeInterval(self.longClickTriggerDuration, target: self, selector: #selector(longClickPerformed), userInfo: nil, repeats: false)
    super.touchesBegan(touches, withEvent: event)
}

/// Intercepts touch ended action.
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    longClickTimer?.invalidate()
    super.touchesEnded(touches, withEvent: event)
}

/// Intercepts touch moved action.
override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    longClickTimer?.invalidate()
    super.touchesMoved(touches, withEvent: event)
}

/// Intercepts touch cancelled action.
override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
    longClickTimer?.invalidate()
    super.touchesCancelled(touches, withEvent: event)
}

Then somewhere in controller of our collection view declaring callback listener:

let longClickListener: OnLongClickListener = {view in
    print("Long click was performed!")
}

And finally in cellForItemAtIndexPath setting callback for our cells:

/// Data population.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath)
    let castedCell = cell as? OurCellView
    castedCell?.setOnLongClickListener(longClickListener)

    return cell
}

Now we can intercept long click actions on our cells.

Andrei K.
  • 294
  • 4
  • 16
0

Easier solution.

In your cellForItemAt delegate (set .tag property for later):

cell.gestureRecognizers?.removeAll()
cell.tag = indexPath.row
let directFullPreviewer = UILongPressGestureRecognizer(target: self, action: #selector(directFullPreviewLongPressAction))
cell.addGestureRecognizer(directFullPreviewer)

And callback for longPress:

@objc func directFullPreviewLongPressAction(g: UILongPressGestureRecognizer)
{
    if g.state == UIGestureRecognizer.State.began
    {
        // Get index as g.view.tag and that's it
    }
}
sabiland
  • 2,188
  • 1
  • 21
  • 22