user994673
user994673

Reputation: 55

UIGestureRecognizer on custom UITableViewCell. Tapping fires on multiple cells

I've searched high and low to no avail on this odd issue. I have a custom subclassed UITableViewCell. Two views for a front and back, and when tapped the cell flips over. This is done like so:

    - (IBAction)cellFrontTapped:(id)sender {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    if (_cellFlag == 0)
    {
        _cellFlag = 1;
        [UIView transitionFromView:_cellFrontView toView:_cellBackView
                          duration:0.5
                           options:UIViewAnimationOptionTransitionFlipFromTop | UIViewAnimationOptionShowHideTransitionViews
                        completion:NULL];
        [self performSelector:@selector(cellBackTapped:) withObject:self afterDelay:15.0];
    }
}
- (IBAction)cellBackTapped:(id)sender {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    if (_cellFlag == 1)
    {
        _cellFlag = 0;
        [UIView transitionFromView:_cellBackView toView:_cellFrontView
                          duration:0.5
                           options:UIViewAnimationOptionTransitionFlipFromBottom | UIViewAnimationOptionShowHideTransitionViews
                        completion:NULL];
    }
}

This works fine with just a few cells. Up to a few pages worth. But when I load a larger data set, tapping on one cell flips it over like expected, but also flips over other cells. I realize this is because the cells are being re-used by dequeueReusableCellWithIdentifier but I cannot decipher how to prevent it.

I've tried implementing it as one UIGestureRecognizer. I've tried utilizing didSelectRowAtIndexPath like this:

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCell *cell = (MyTableViewCell*)[tableView cellForRowAtIndexPath:indexPath];

    if (cell.cellFlag == 0)
    {
        [cell cellFrontTapped];
    }
    else
    {
        [cell cellBackTapped];
    }
}

These all work to flip the cell properly, but the tap event always flips other cells in the list. The only solution I've found is to not use dequeueReusableCellWithIdentifier like so:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyTableViewCell"];
    if (cell == nil) {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"MyTableViewCell" owner:self options:nil];
        cell = [nib objectAtIndex:0];
    }

    [self configureCell:cell atIndexPath:indexPath];

    return cell;
}

Here's my current subclass implementation:

@implementation MyTableViewCell

- (void)awakeFromNib {
    // Adding the recognizer here produced the same result
    //UIGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cellTapped)];
    //tap.delegate = self;
    //[self addGestureRecognizer:tap];
    _cellFlag = 0;
}

- (void)cellFrontTapped {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    _cellFlag = 1;
    [UIView transitionFromView:_cellFrontView toView:_cellBackView
                          duration:0.5
                           options:UIViewAnimationOptionTransitionFlipFromTop | UIViewAnimationOptionShowHideTransitionViews
                        completion:NULL];
    [self performSelector:@selector(cellBackTapped) withObject:self afterDelay:15.0];
}

- (void)cellBackTapped {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    _cellFlag = 0;
    [UIView transitionFromView:_cellBackView toView:_cellFrontView
                          duration:0.5
                           options:UIViewAnimationOptionTransitionFlipFromBottom | UIViewAnimationOptionShowHideTransitionViews
                        completion:NULL];
}

-(void)cellTapped
{
    if (_cellFlag == 0)
    {
        [self cellFrontTapped];
    }
    else
    {
        [self cellBackTapped];
    }
}

@end

Here's the `configureCell:atIndexpath:

- (void)configureCell:(MyTableViewCell*)cell atIndexPath:(NSIndexPath*)indexPath
{
    NSManagedObject *t = [[self fetchedResultsController] objectAtIndexPath:indexPath];
    //here i set the labels and such
    cell.cellBackAmountLabel.text = [t.amount formattedAmount];
    //added 
    if ([self.flippedCellIndexes containsObject:indexPath])
    {
        [cell.contentView bringSubviewToFront:cell.cellBackView];
    }
    else
    {
        [cell.contentView bringSubviewToFront:cell.cellFrontView];
    }
}

I'm at a loss. Not using dequeueReusableCellWithIdentifier makes a significant performance impact in my case. Help!

Upvotes: 1

Views: 327

Answers (1)

André Fratelli
André Fratelli

Reputation: 6068

I would recommend separating your model from your view. What this means is that you should keep track of which cells should render as flipped, but not on the cell itself. Lets say you declare an NSArray (probably NSMutableArray, even) called flippedIndexes in your view controller.

If your data set is as big as you say, then probably you should use a sparse array instead of NSArray. Sparse arrays can easily be implemented with NSMutableDictionary. NSMutableSet could also work.

When dequeueing the cell, you would then check which cells should be flipped, probably at that configureCell:atIndexPath: method of yours. Basically, if the indexPath shows on your sparse array or set you render the cell as flipped. This would imply dropping the cellFlag property you have declared on your cell and toggling its state according to the model I've been mentioning. For flipping a cell, check the flippedIndexes property for the given indexPath and act accordingly. Something like this:

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyTableViewCell *cell = (MyTableViewCell*)[tableView cellForRowAtIndexPath:indexPath];

    if (![self.flippedIndexes containsObject:indexPath])
    {
        [cell cellFrontTapped];
        [self.flippedIndexes addObject:indexPath];
    }
    else
    {
        [cell cellBackTapped];
        [self.flippedIndexes removeObject:indexPath];
    }
}

Here I'm assuming the use of NSMutableSet for the flippedIndexes property. Notice that this will only work properly if you also check the model in configureCell:atIndexPath:, as otherwise you'll have cells magically clearing when scrolling.

Moral of the story is: don't store state information in queued cells.

Upvotes: 2

Related Questions