2

I have a UICollectionView that has elements that can be dragged and dropped around the screen. I use a UILongPressGestureRecognizer to handle the dragging. I attach this recognizer to the collection view cells in my collectionView:cellForItemAtIndexPath: method. However, the recognizer's view property occasionally returns a UIView instead of a UICollectionViewCell. I require some of the methods/properties that are only on UICollectionViewCell and my app crashes when a UIView is returned instead.

Why would the recognizer that is attached to a cell return a plain UIView?

Attaching the recognizer

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{
    EXSupplyCollectionViewCell *cell = (EXSupplyCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
    UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:cell action:nil];
    longPressRecognizer.delegate = self;
    [cell addGestureRecognizer:longPressRecognizer];
    return cell;
}

Handling the gesture

I use a method with a switch statement to dispatch the different states of the long press.

- (void)longGestureAction:(UILongPressGestureRecognizer *)gesture {
    UICollectionViewCell *cell = (UICollectionViewCell *)[gesture view];
    switch ([gesture state]) {
        case UIGestureRecognizerStateBegan:
            [self longGestureActionBeganOn:cell withGesture:gesture];
            break;
        //snip
        default:
            break;
    }
}

When longGestureActionBeganOn:withGesture is called if cell is actually a UICollectionViewCell the rest of the gesture executes perfectly. If it isn't then it breaks when it attempts to determine the index path for what should be a cell.

First occurrence of break

- (void)longGestureActionBeganOn:(UICollectionViewCell *)cell withGesture:(UILongPressGestureRecognizer *)gesture
{
    NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell]; // unrecognized selector is sent to the cell here if it is a UIView
    [self.collectionView setScrollEnabled:NO];
    if (indexPath != nil) {
        // snip
    }
}

I also use other properties specific to UICollectionViewCell for other states of the gesture. Is there some way to guarantee that the recognizer will always give me back the view that I assigned it to?

Community
  • 1
  • 1
Blake Hair
  • 151
  • 1
  • 13
  • 1
    The problem is probably related to cell reuse, with your code you will end up with multiple gestureRecognizers per cell if a cell is about to be displayed. In theory this should only trigger the action multiple times and not mix up views. Anyway, I would recommend to [add the gestureRecognizer to the collectionView](http://stackoverflow.com/questions/18848725/long-press-gesture-on-uicollectionviewcell/18848817#18848817) instead. – Matthias Bauch Jun 08 '14 at 19:30
  • @MatthiasBauch I removed the gesture recognizer in the `prepareForReuse` method to prevent multiple additions of recognizers and it appears to have solved my problem. If you'd like to make an answer out of your comment I'll go ahead and mark it as the solution. – Blake Hair Jun 08 '14 at 19:34

2 Answers2

5

Views like UICollectionView and UITableView will reuse their cells. If you blindly add a gestureRecognizer in collectionView:cellForItemAtIndexPath: you will add a new one each time the cell is reloaded. If you scroll around a bit you will end up with dozens of gestureRecognizers on each cell.

In theory this should not cause any problems besides that the action of the gestureRecognizer is called multiple times. But Apple uses heavy performance optimization on cell reuse, so it might be possible that something messes up something.

The preferred way to solve the problem is to add the gestureRecognizer to the collectionView instead.

Another way would be to check if there is already a gestureRecognizer on the cell and only add a new one if there is none. Or you use the solution you found and remove the gestureRecognizer in prepareForReuse of the cell. When you use the latter methods you should check that you remove (or test for) the right one. You don't want to remove gestureRecognizers the system added for you. (I'm not sure if iOS currently uses this, but to make your app proof for the future you might want to stick to this best practice.)

Community
  • 1
  • 1
Matthias Bauch
  • 89,811
  • 20
  • 225
  • 247
  • Check this way: if ([cell.gestureRecognizers count]) { // recognizer(s) already found in cell } – JRam13 Dec 10 '15 at 19:05
0

I had a similar problem related to Long-Touch. What I ended up doing is override the UICollectionViewCell.PrepareForReuse and cancel the UIGestureRecognizers attached to my view. So everytime my cell got recycled a long press event would be canceled.

See this answer

Ganso Doido
  • 587
  • 1
  • 6
  • 11