UICollectionView UIPanGestureRecognizer Swipe Up Gesture

Alex Egg,

I had the task to create a swipe up gesture to my UIViewCollectionCell which will trigger an action. See the wireframe below:

wireframe pic

#Setup
This is how I accomplished the task using UIPanGestureRecognizer.

The buttons in a row are made up of a UICollectionView. During my main view viewDidLoad method I setup the gesture recognition on the UICollectionView as a whole, as opposed to attaching a recognizer on each cell per apple’s design recommendations:

- (void)viewDidLoad
{
    [super viewDidLoad];
  
    //other app logic...
    //.
    //.
    //.
    //other app logic...
    
    //datasource
    self.patternImagesArray =[[NSMutableArray alloc]init];
    
    UIPanGestureRecognizer * panner=[[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(handlePan:)];
    panner.delegate=self;
    [self.view addGestureRecognizer:panner];

}

The important parts here are to set the selector for the recognizer, which is the event handler for when the user starts swiping. Also make sure you set the UIPanGestureRecognizer delegate so you will get certain callbacks you will need (below). Then finally add the recognizer to your main view.

Now, we need to implement our event handler for when the swiping happens on the collection view:

- (void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer
{
    
    CGPoint swipeLocation = [gestureRecognizer locationInView:myCollectionView];
    NSIndexPath *swipedIndexPath = [myCollectionView indexPathForItemAtPoint:swipeLocation];
    UdioPatternViewCell* swipedCell = [myCollectionView cellForItemAtIndexPath:swipedIndexPath];
    
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
    {
        // Start of the gesture.
        // You could remove any layout constraints that interfere
        // with changing of the position of the content view.
    }
    else if (gestureRecognizer.state == UIGestureRecognizerStateChanged)
    {
        //if you swipe outside of the bounds of the collection view,
        //we can't resolve which cell it's over. So this ignores any
        //swipes outside the view and saves a ref to the last valid cell
        if(!swipedCell)
            return;

        _last_cell=swipedCell;
        
        
        CGFloat velocity_y = [gestureRecognizer velocityInView:self.view].y;
        CGPoint translation = [gestureRecognizer translationInView:self.view];
        CGPoint imageViewPosition = swipedCell.center;
        //imageViewPosition.x += translation.x;
        imageViewPosition.y += translation.y;
        
        swipedCell.center = imageViewPosition;
        [gestureRecognizer setTranslation:CGPointZero inView:self.view];
        
        //NSLog(@"x:%f, y:%f, v:%f", imageViewPosition.x, imageViewPosition.y, velocity_y);
        
        BBCard * card =[self.patternImagesArray objectAtIndex:swipedIndexPath.row];
        
        //debouncing
        if(imageViewPosition.y > 25 && (card != _last_broadcasted_card))
        {
            [self queue_broadcast:card];
            _last_broadcasted_card=card;
        }
        
        
    }
    else if (gestureRecognizer.state == UIGestureRecognizerStateEnded)
    {
        // Dragging has ended.
        // You could add layout constraints back to the content view here.

        
        CGPoint imageViewPosition = _last_cell.center;
        imageViewPosition.y=35;
        _last_cell.center=imageViewPosition;
          [gestureRecognizer setTranslation:CGPointZero inView:self.view];

        
        // NSLog(@"Restoring to: x:%f, y:%f", imageViewPosition.x, imageViewPosition.y);

    }
}

So there are 3 states that the panning even can be in:

  1. It just started
  2. It’s happening
  3. It’s done

Most of the logic goes into state 2 and the cleanup logic in in state 3.

In the beginning of the event handler we try to figure out what cell the user clicked on:

    CGPoint swipeLocation = [gestureRecognizer locationInView:myCollectionView];
    NSIndexPath *swipedIndexPath = [myCollectionView indexPathForItemAtPoint:swipeLocation];
    UdioPatternViewCell* swipedCell = [myCollectionView cellForItemAtIndexPath:swipedIndexPath];

We first get a x,y location of the swipe and then use the UICollectionView method indexPathForItemAtPoint which can resolve a x,y location to an actual cell in the collection view – very handy. From there we get an actual UICollectionViewCell from the NSIndexPath.

Now that we know the cell we are dealing with we can move it around under the user’s finger by setting it’s center property.

        CGFloat velocity_y = [gestureRecognizer velocityInView:self.view].y;
        CGPoint translation = [gestureRecognizer translationInView:self.view];
        CGPoint imageViewPosition = swipedCell.center;
        //imageViewPosition.x += translation.x;
        imageViewPosition.y += translation.y;
        
        swipedCell.center = imageViewPosition;
        [gestureRecognizer setTranslation:CGPointZero inView:self.view];

Note how the x translation is commented out – I only care about swiping up so i filter out x movemements. Then if the swipe is beyond a certain threshold I invoke my desired action on it:

        //debouncing
        if(imageViewPosition.y > 25 && (card != _last_broadcasted_card))
        {
            [self queue_broadcast:card]; //act on this swipe
            _last_broadcasted_card=card;
        }

After the cell is swiped up past point 25 and the same action hasn’t been invoked on it on the last check, we act on the swipe. The rest of the code is poor-man’s debouncing, so the repeated swipe callbacks don’t trigger the action more than once.

The last state, #3, is the cleanup code which puts the cell back in it’s starting place:

CGPoint imageViewPosition = _last_cell.center;
imageViewPosition.y=35;
_last_cell.center=imageViewPosition;
[gestureRecognizer setTranslation:CGPointZero inView:self.view];

This is a rush job here, as I just hardcoded the start position to 35 points, which I know from the IB designer.

#The Problem

Now run the program and you will notice a small error. My custom swipe recognizer blocks the UICollectionView’s default horizontal swipe recognizer. The way to fix this problem is to implement the gestureRecognizerShouldBegin in the UICollectionView delegate. This method will you decide to run your recognizer or let it bubble-up to the UICollectionView.

-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    CGPoint translation =[gestureRecognizer translationInView:self.view];
    BOOL flag=(translation.x * translation.x < translation.y * translation.y);
    return flag;
}

So this is saying if the magnitude of y movement is larger than the magnitude of x movements, consider this swipe up and let my custom recognizer handle it, otherwise let the UICollectionView handle the swipe.

#Final Product

wireframe pic


Permalink: UICollectionView-UIPanGestureRecognizer-Swipe-Up-Gesture

Tags:

Last edited by Alex Egg, 2016-10-05 19:02:29
View Revision History