Search code examples
objective-cuitableview

Reuse a tableview cell with CALayer


I have a tableview cell with a reuse-identifier set in storyboard. After checking some conditions I want to draw different things on the contentView of the cell. My problem is when the tableview re-uses the cell, the CAShapeLayer from another cell persists, so it's showing the new drawing over the (re-used) drawing.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StatusCell"];

    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];

    if ([[object valueForKey:@"status"] isEqualToString:@"sending"]) {
        CAShapeLayer *progressLayer;
        //...Configure and draw Layer with UIBezierPath
        [cell.contentView.layer addSublayer:progressLayer];
    } else if ([[object valueForKey:@"status"] isEqualToString:@"sent"]) {
        CAShapeLayer *sentLayer;
        //...Configure and draw Layer with UIBezierPath
        [cell.contentView.layer addSublayer:sentLayer];
    }

    return cell;
}

So I tried to give the CAShapeLayer a name and delete the layer from the re-used cell before creating a new CAShapeLayer.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StatusCell"];

    NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];

    for (CALayer *layer in cell.contentView.layer.sublayers) {
        if ([layer.name isEqualToString:@"sending"] || [layer.name isEqualToString:@"sent"]) {
            [layer removeFromSuperlayer];
        }
    }

    if ([[object valueForKey:@"status"] isEqualToString:@"sending"]) {
        CAShapeLayer *progressLayer;
        progressLayer.name = @"sending";
        //...Configure and draw Layer with UIBezierPath
        [cell.contentView.layer addSublayer:progressLayer];
    } else if ([[object valueForKey:@"status"] isEqualToString:@"sent"]) {
        CAShapeLayer *sentLayer;
        sentLayer.name = @"sent";
        //...Configure and draw Layer with UIBezierPath
        [cell.contentView.layer addSublayer:sentLayer];
    }

    return cell;
}

But with this method I get an exception when scrolling the tableview CALayerArray was mutated while being enumerated

Can someone point me in the right direction how to do it the correct way?


Solution

  • I would advise you to subclass UITableViewCell, assign these layers as properties, and manage their visibility using hidden property of CALayer.

    Create a custom cell class

    @interface StatusCell: UITableViewCell
    
    @property (nonatomic, strong) CAShapeLayer *progressLayer;
    @property (nonatomic, strong) CAShapeLayer *sentLayer;
    
    @end
    

    Assign StatusCell class name to your cell prototype in storyboard.

    Create the layers – as you could notice, cellForRowAtIndexPath method is not the best place to do it, as it is called often during cell life cycle. I usually create properties in init methods, as your cell is storyboard-based, you can also use the awakeFromNib method, it is also called only once during nib-based cells' life cycle.

    @implementation StatusCell
    
    - (void)awakeFromNib {
        [super awakeFromNib];
    
        CAShapeLayer *progressLayer;
        // configuraion
        progressLayer.hidden = YES;
        self.progressLayer = progressLayer;
    
        // repeat for sentLayer.
    }
    
    @end
    

    In your cellForRow method you set one layer to be hidden, and other to be visible. You can adjust layers' positions, if required.

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        StatusCell *cell = [tableView dequeueReusableCellWithIdentifier:@"StatusCell"];
    
        // Optionally adjust your layers' frames if your cell has dynamic height. 
    
        NSManagedObject *object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    
        cell.progressLayer.hidden = ![[object valueForKey:@"status"] isEqualToString:@"sending"];
        cell.sentLayer.hidden = ![[object valueForKey:@"status"] isEqualToString:@"sent"];
    
        return cell;
    }