objective-cuicollectionviewuipangesturerecognizer

Highlight UICollectionViewCells with PanGesture using Objective-C


I'm using the below code to track cells when a user drags their finger over them (this works great). That said, I want to highlight each of the cells (or change the color of the background view in my custom cells) as the user drags their finger over top of them. How can I accomplish this? See below.

ViewController.m

- (void)viewDidLoad
{
    [super viewDidLoad];

    UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
    [self.ringCollectionView addGestureRecognizer:panGesture];
}


- (void) handlePanGesture:(UIPanGestureRecognizer*) panGesture
{
    CGPoint location = [panGesture locationInView:self.ringCollectionView];

    NSIndexPath *indexPath = [self.ringCollectionView indexPathForItemAtPoint:location];
    NSMutableArray *selectedIndexes = [NSMutableArray arrayWithArray:[self.ringCollectionView indexPathsForSelectedItems]];

    if (![selectedIndexes containsObject:@(indexPath.row)]) {

        NSLog(@"THIS CELL IS %ld", (long)indexPath.row);
    }
    else
        if (panGesture.state == UIGestureRecognizerStateEnded) {

        }
}


- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 10;
}


- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"RingCollectionViewCell";
    RingCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellIdentifier forIndexPath:indexPath];

    return cell;
}


- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"Tapping");
}


- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return CGSizeMake(118, 118);
}

Solution

  • First, I believe tracking selected cells via "native" UICollectionView mechanism is reasonable and consistent. There isn’t any need for custom gesture handlers inside of the cells. In order to represent selected state however, you need to set selectedBackgroundView property, something like this:

    - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"RingCollectionViewCell" forIndexPath:indexPath];
        if (!cell.selectedBackgroundView) {
            cell.selectedBackgroundView = [[UIView alloc] initWithFrame:cell.bounds];
            cell.selectedBackgroundView.backgroundColor = [UIColor grayColor];
        }
        return cell;
    }
    

    I don't know how your RingCollectionViewCell class is designed, but be advised that you will need to ensure contentView of this cell has transparent background color otherwise it will obscure the selectedBackgroundView.

    Now the tricky part. UIPanGestureRecognizer gets events very quickly and the handle method will be called quite often when the user just keeps his/her fingers still on the screen. Thus you need to somehow suppress events which are not supposed to switch selected state of a cell.

    My suggestion is to ignore all consequence events if they happen in the same cell. In order to implement such behavior we need to store index path of the last cell touched. Let’s use a simple property for that:

    @interface ViewController ()
    
    @property (strong, nonatomic, nullable) NSIndexPath *trackingCellIndexPath;
    
    @end
    

    Now you need to assign index path of last touched cell to this property in your gesture handle method. You also need to reset this property when the gesture is finished or cancelled. Switch selection of the given index path if it is not equal to the tracked index path and ignore the touch event otherwise:

    - (void)handlePanGesture:(UIPanGestureRecognizer *)recognizer {
        // Reset the tracking state when the gesture is finished
        switch (recognizer.state) {
            case UIGestureRecognizerStateEnded:
            case UIGestureRecognizerStateCancelled:
                self.trackingCellIndexPath = nil;
                return;
            default:
                break;
        }
    
        // Obtain the cell the user is currently dragging over
        CGPoint location = [recognizer locationInView:self.collectionView];
        NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:location];
    
        // If the user currently doesn't touch any cell, reset the tracking state and prepare to listen to another cell
        if (!indexPath) {
            if (self.trackingCellIndexPath) {
                self.trackingCellIndexPath = nil;
            }
            return;
        }
    
        // If current event is subsequent gesture event which happens within the same cell, ignore it
        if (self.trackingCellIndexPath == indexPath) {
            return;
        }
    
        // If the cell hasn't been previously tracked, switch the selected state and start tracking it
        self.trackingCellIndexPath = indexPath;
        if ([self.collectionView.indexPathsForSelectedItems containsObject:indexPath]) {
            [self.collectionView deselectItemAtIndexPath:indexPath animated:YES];
        } else {
            [self.collectionView selectItemAtIndexPath:indexPath animated:YES scrollPosition:UICollectionViewScrollPositionNone];
        }
    }
    

    The last thing to change is the selection mode of the collection view. As you expect multiple cells to remain selected, just switch allowsMultipleSelection of your collection view somewhere in viewDidLoad:

    - (void)viewDidLoad {
        ...
        self.collectionView.allowsMultipleSelection = YES;
    }