Creating a draggable UICollectionViewCell

July 16, 2014

So here’s the situation - you’re creating an interactive UICollectionView, and you want to be able to drag a cell around the screen with a touch. To provide user feedback, you want the contents of the cell to follow the user’s finger as it moves around.

The problem is that unless you’re using a completely custom collection view layout, you can’t move the cell itself. The collection view is in charge of where things are displayed, and it’s a major pain to override this - especially if you’re using a flow layout. Reimplementing UICollectionViewFlowLayout from scratch is a decidedly non-trivial undertaking.

The answer lies in a hack. Create a copy of the contents of the cell as an image, then drag this around the screen underneath your finger. Much easier.

Here’s an example - it assumes that you’ve previously created and attached a UIPanGestureRecognizer to the collection view, and tied this to a method called handlePan: in your view controller. There’s also a UIImageView property on the view controller called movingCell.

``` objc A draggable UICollectionViewCell https://gist.github.com/31e2e5bd75e99405bc35.git link (void)handlePan:(UIPanGestureRecognizer *)panRecognizer {

CGPoint locationPoint = [panRecognizer locationInView:self.collectionView];

if (panRecognizer.state == UIGestureRecognizerStateBegan) {

    NSIndexPath indexPathOfMovingCell = [self.collectionView indexPathForItemAtPoint:locationPoint];
    UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPathOfMovingCell];

    UIGraphicsBeginImageContext(cell.bounds.size);
    [cell.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *cellImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    self.movingCell = [[UIImageView alloc] initWithImage:cellImage];
    [self.movingCell setCenter:locationPoint];
    [self.movingCell setAlpha:0.75f];
    [self.collectionView addSubview:self.movingCell];

}

if (panRecognizer.state == UIGestureRecognizerStateChanged) {
    [self.movingCell setCenter:locationPoint];
}

if (panRecognizer.state == UIGestureRecognizerStateEnded) {
    [self.movingCell removeFromSuperview];
}

} ```

When the pan gesture recognizer fires, it calls the handlePan: method with itself as a parameter.

A UIPanGestureRecognizer has three states that we’re interested in - UIGestureRecognizerStateBegan (which is fired as the first touch starts), UIGestureRecognizerStateChanged(which fires as the touch moves) and UIGestureRecognizerStateEnded (which fires as the finger is lifted).

We hook into the UIGestureRecognizerStateBegan event, and get the location where the pan gesture is occurring:

CGPoint locationPoint = [panRecognizer locationInView:self.collectionView];

Then if the touches have just begun, we grab the cell in which the touch started:

NSIndexPath *indexPathOfMovingCell = [self.collectionView indexPathForItemAtPoint:locationPoint];

UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPathOfMovingCell];

and create a UIImage out of the cell’s layer:

UIGraphicsBeginImageContext(cell.bounds.size);

[cell.layer renderInContext:UIGraphicsGetCurrentContext()];

UIImage *cellImage = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

Finally, we use this UIImage to populate a UIImageView property, and update the center of the UIImageView so that it lies underneath the current location of the touch. I’ve also tweaked the image’s opacity to make it slightly translucent:

self.movingCell = [[UIImageView alloc] initWithImage:cellImage];

[self.movingCell setCenter:locationPoint];

[self.movingCell setAlpha:0.75f];

[self.collectionView addSubview:self.movingCell];

Following the touches is just a case of updating the centre of the UIImageView:

if (panRecognizer.state == UIGestureRecognizerStateChanged) {

    [self.movingCell setCenter:locationPoint];

}

And when the touches end, we remove the UIImageView from the collectionView completely:

if (panRecognizer.state == UIGestureRecognizerStateEnded) {

    [self.movingCell removeFromSuperview];

}

This implementation simply removes the pseudo-cell from the screen when the touch finishes, but there’s no reason why you can’t do something like insert it back into the collection view at the point where it was ‘dropped’. I’ll put the code for this up in another post.