6

With a similar problem to this question, I am trying to add a double tap gesture recognizer to my UICollectionView instance.

I need to prevent the default single tap from calling the UICollectionViewDelegate method collectionView:didSelectItemAtIndexPath:.

In order to achieve this I implement the code straight from Apple's Collection View Programming Guide (Listing 4-2):

UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
NSArray* recognizers = [self.collectionView gestureRecognizers];

// Make the default gesture recognizer wait until the custom one fails.
for (UIGestureRecognizer* aRecognizer in recognizers) {
   if ([aRecognizer isKindOfClass:[UITapGestureRecognizer class]])
      [aRecognizer requireGestureRecognizerToFail:tapGesture];
}

// Now add the gesture recognizer to the collection view.
tapGesture.numberOfTapsRequired = 2;
[self.collectionView addGestureRecognizer:tapGesture];

This code does not work as expected: tapGesture fires on a double tap but the default single tap is not prevented and the delegate's didSelect... method is still called.

Stepping through in the debugger reveals that the if condition, [aRecognizer isKindOfClass:[UITapGestureRecognizer class]], never evaluates to true and so the failure-requirement on the new tapGesture is not being established.

Running this debugger command each time through the for-loop:

po (void)NSLog(@"%@",(NSString *)NSStringFromClass([aRecognizer class]))

reveals that the default gesture recognizers are (indeed) not UITapGestureRecognizer instances.

Instead they are private classes UIScrollViewDelayedTouchesBeganGestureRecognizer and UIScrollViewPanGestureRecognizer.

First, I can't use these explicitly without breaking the rules about Private API. Second, attaching to the UIScrollViewDelayedTouchesBeganGestureRecognizer via requireGestureRecognizerToFail: doesn't appear to provide the desired behaviour anyway — i.e. the delegate's didSelect... is still called.

How can I work with UICollectionView's default gesture recognizers to add a double tap to the collection view and prevent the default single tap from also firing the delegate's collectionView:didSelectItemAtIndexPath: method?

Thanks in advance!

Community
  • 1
  • 1
Carlton Gibson
  • 7,278
  • 2
  • 36
  • 46
  • Isn't not implementing `collectionView:didSelectItemAtIndexPath:` enough? – cahn Jul 16 '13 at 08:37
  • @cahn: Afraid not — I need the double tap in addition to the standard behaviour. – Carlton Gibson Jul 16 '13 at 10:44
  • [This question](http://stackoverflow.com/questions/12792661/how-to-detect-double-taps-on-cells-in-a-uicollectionview) seems to solve the same issue you're having. Does it work? – Ash Furrow Jul 16 '13 at 13:06
  • 1
    @AshFurrow: I'm afraid not — My double tap IS working but I (too) CAN'T stop didSelect... getting called as well. The suggestion about shouldSelect... and shouldDeselect... looks interesting but the "erm..., this isn't right" comment leaves me a little unsure how to proceed. – Carlton Gibson Jul 16 '13 at 18:44

2 Answers2

4

My solution was to not implement collectionView:didSelectItemAtIndexPath but to implement two gesture recognizers.

    self.doubleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(processDoubleTap:)];
    [_doubleTapGesture setNumberOfTapsRequired:2];
    [_doubleTapGesture setNumberOfTouchesRequired:1];   

    [self.view addGestureRecognizer:_doubleTapGesture];

    self.singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(processSingleTap:)];
    [_singleTapGesture setNumberOfTapsRequired:1];
    [_singleTapGesture setNumberOfTouchesRequired:1];
    [_singleTapGesture requireGestureRecognizerToFail:_doubleTapGesture];

    [self.view addGestureRecognizer:_singleTapGesture];

This way I can handle single and double taps. The only gotcha I can see is that the cell is selected on doubleTaps but if this bothers you can you handle it in your two selectors.

Paul Cezanne
  • 8,629
  • 7
  • 59
  • 90
  • Thanks for the answer. It seems this is the way to go. – Carlton Gibson Aug 06 '13 at 09:02
  • I wouldn't recommend this solution because of following reasons: 1) You may have existing code, that already uses selection 2) You may want selection in the future 3) gestureRecognizers meant to be used in any views and this views can have all different logic for handling touch events. Using delaysTouchesBegan/Ended is much more scalable solution. ( See Szilto's answer) – Stepan Generalov Jun 13 '15 at 23:39
  • I agree now, but back in '13 I'm pretty sure I tried that and never got it to work. I could be wrong obviously but I like Sz'z code also. – Paul Cezanne Jun 14 '15 at 03:23
  • Single tap gesture can be used instead of collectionView:didSelectItemAtIndexPath only in case the selection is fast. In case the user is touching the cell for a while and then release the finger, the didSelectItemAtIndexPath will be called but not your tap gesture so it might frustrate some users – Mayosse Jun 07 '16 at 14:17
4

I use the following to register a UITapGestureRecognizer:

UITapGestureRecognizer* singleTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTapGesture:)];
singleTapGesture.delaysTouchesBegan = YES;
singleTapGesture.numberOfTapsRequired = 1; // number of taps required
singleTapGesture.numberOfTouchesRequired = 1; // number of finger touches required
[self.collectionView addGestureRecognizer:singleTapGesture];

By setting delaysTouchesBegan to YES the custom gesture recognizer gets priority over the default collection view tap listeners by delaying the registering of other touch events. Alternatively, you can set cancel touch recognition altogether by setting the cancelsTouchesInView to YES.

The gesture is than handled by the following function:

- (void)handleSingleTapGesture:(UITapGestureRecognizer *)sender {

    if (sender.state == UIGestureRecognizerStateEnded) {
        CGPoint location = [sender locationInView:self.collectionsView];
        NSIndexPath *indexPath = [self.collectionsView indexPathForItemAtPoint:location];

        if (indexPath) {
            NSLog(@"Cell view was tapped.");
            UICollectionViewCell *cell = [self.collectionsView cellForItemAtIndexPath:indexPath];
            // Do something.                
        }
    }
    else{
        // Handle other UIGestureRecognizerState's
    }
}
Szilto
  • 51
  • 4