How to pass pan gesture to UICollectionVIew from UICollectionViewCell?

Erika Electra picture Erika Electra · May 21, 2014 · Viewed 15.1k times · Source

I have a UICollectionView implementing a grid-based layout of custom UICollectionViewCells. To allow cells to respond to dragging, I individually add a UIPanGestureRecognizer to each cell.

The UICollectionView still scrolls (horizontally) when I touch down and swipe left/right starting at points between cells, but as long as the pan gesture recognizer is added to a cell, it seems like the CollectionView refuses to scroll when I start my swipe tapping within a cell.

Right now, I separate horizontal left/right drags from vertical up/down drags, so there should not be any conflict between dragging cells out (vertical swipes) and scrolling the CollectionView (Horizontal swipes). In this case, how can I pass the swipe to the collection/scroll view so it knows to scroll like normal? It's really annoying to have to start on the boundary or spacing between cells.

Once I remove the pan gesture from a cell, scrolling works as normal no matter if I start swiping on a cell or between cells.

EDIT:Desired pan gesture behavior posted below as current code

// Handle pans by detecting swipes:
-(void) handlePan:(UIPanGestureRecognizer*)recognizer
{
    // Calculate touch location
    CGPoint touchXY = [recognizer locationInView:masterWindowView];

    // Handle touch
    if (recognizer.state == UIGestureRecognizerStateBegan)
    {
        gestureWasHandled = NO;
        pointCount = 1;
        startPoint = touchXY;
    }

    if (recognizer.state == UIGestureRecognizerStateChanged)
    {
        ++pointCount;

        // Calculate whether a swipe has occurred
        float dX = deltaX(touchXY, startPoint);
        float dY = deltaY(touchXY, startPoint);

        BOOL finished = YES;
        if ((dX > kSwipeDragMin) && (ABS(dY) < kDragLimitMax)) {
            touchType = TouchSwipeLeft;
            NSLog(@"LEFT swipe detected");
            [recognizer requireGestureRecognizerToFail:recognizer];
            //[masterScrollView handlePan]
        }
        else if ((dX < -kSwipeDragMin) && (ABS(dY) < kDragLimitMax)) {
            touchType = TouchSwipeRight;
            NSLog(@"RIGHT swipe detected");
            [recognizer requireGestureRecognizerToFail:recognizer];
        }
        else if ((dY > kSwipeDragMin) && (ABS(dX) < kDragLimitMax)) {
            touchType = TouchSwipeUp;
            NSLog(@"UP swipe detected");
        }
        else if ((dY < -kSwipeDragMin) && (ABS(dX) < kDragLimitMax)) {
            touchType = TouchSwipeDown;
            NSLog(@"DOWN swipe detected");
        }
        else
            finished = NO;

        // If unhandled and downward, produce a new draggable view
        if (!gestureWasHandled && finished && (touchType == TouchSwipeDown))
        {

            [self.delegate cellBeingDragged:self];
            dragView.center = touchXY;
            dragView.hidden = NO;
            dragView.backgroundColor = [UIColor clearColor];


            masterScrollView.scrollEnabled = NO; // prevent user from scrolling during
            gestureWasHandled = YES;
        }
        else if (gestureWasHandled)
        {
            // allow continued dragging after detection
            dragView.center = touchXY;
        }
    }

    if (recognizer.state == UIGestureRecognizerStateEnded)
    {
        // ensure that scroll view returns to scrollable
        if (gestureWasHandled) {
            [self.delegate cell:self dragEndedAt:touchXY];
        }
    }
}

// Allow simultaneous recognition
-(BOOL) gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
{
    return YES;
}

This code works when given to each individual cell. It does NOT work when attached to the UICollectionView as its gesture recognizer, and it in fact stops all scrolling.

Answer

KerrM picture KerrM · May 21, 2014

Instead of attaching a UIPanGestureRecognizer to each cell (which will decrease performance) add a UIPanGestureRecognizer to the UICollectionView and when the pan gesture happens use locationInView to get the point in the UICollectionView where the pan started, and then indexPathForItemAtPoint which will return you the index path for the cell you should animate.

This way, you will have only one gesture recognizer (good!) for your whole collection view while also maintaining the control in your view controller (as you wanted) - double win!

Using this solution, in your view controller you would implement gestureRecognizer:shouldReceiveTouch:, grab the given gestureRecognizer, make sure it's your UIPanGestureRecognizer and use its translationInView: method to find out if the translation is on the X or Y axis. Use that information to decide whether you want to return YES or NO. For example:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
  if([gestureRecognizer isEqual:myTapGesture]) {
    CGPoint point = [gestureRecognizer translationInView:self.collectionView];
    if(point.x != 0) { //adjust this condition if you want some leniency on the X axis
      //The translation was on the X axis, i.e. right/left, 
      //so this gesture recognizer shouldn't do anything about it
      return NO;
    }
  }
  return YES;
}