Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using cached UIView to set cell background view in tableView:willDisplayCell:forRowAtIndexPath:

This is my solution for setting custom grouped table view cell backgrounds:

- (UIView *)top
{
    if (_top) {
        return _top;
    }

    _top = [[UIView alloc] init];
    [_top setBackgroundColor:[UIColor blueColor]];

    return _top;
}

// dot dot dot

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSInteger section = [indexPath section];
    NSInteger row = [indexPath row];
    NSInteger maxRow = [tableView numberOfRowsInSection:section] - 1;

    if (maxRow == 0) {
        [cell setBackgroundView:[self lonely]];
    } else if (row == 0) {
        [cell setBackgroundView:[self top]];
    } else if (row == maxRow) {
        [cell setBackgroundView:[self bottom]];
    } else {
        [cell setBackgroundView:[self middle]];
    }
}

Obviously it doesn't work as expected which brings me here, but it does work when I don't use cached views:

UIView *background = [[UIView alloc] init];

if (maxRow == 0) {
    [background setBackgroundColor:[UIColor redColor]];
} else if (row == 0) {
    [background setBackgroundColor:[UIColor blueColor]];
} else if (row == maxRow) {
    [background setBackgroundColor:[UIColor yellowColor]];
} else {
    [background setBackgroundColor:[UIColor greenColor]];
}

[cell setBackgroundView:background];

UPDATE: After Jonathan pointed out that I can't use the same view for more than one cell, I decided to follow the table view model where it has a queue of reusable cells. For my implementation, I have a queue of reusable background views (_backgroundViewPool):

@implementation RootViewController {
    NSMutableSet *_backgroundViewPool;
}
- (id)initWithStyle:(UITableViewStyle)style
{
    if (self = [super initWithStyle:style]) {
        _backgroundViewPool = [[NSMutableSet alloc] init];

        UITableView *tableView = [self tableView];
        [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
    }

    return self;
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    // Return the number of sections.
    return 6;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.

    if (section == 0) {
        return 1;
    }

    return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
    [[cell textLabel] setText:[NSString stringWithFormat:@"[%d, %d]", [indexPath section], [indexPath row]]];

    return cell;
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    UIView *backgroundView = [cell backgroundView];
    [_backgroundViewPool addObject:backgroundView];
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSInteger section = [indexPath section];
    NSInteger row = [indexPath row];
    NSInteger maxRow = [tableView numberOfRowsInSection:section] - 1;
    UIColor *color = nil;


    if (maxRow == 0) {
        // single cell
        color = [UIColor blueColor];
    } else if (row == 0) {
        // top cell
        color = [UIColor redColor];
    } else if (row == maxRow) {
        // bottom cell
        color = [UIColor greenColor];
    } else {
        // middle cell
        color = [UIColor yellowColor];
    }

    UIView *backgroundView = nil;

    for (UIView *bg in _backgroundViewPool) {
        if (color == [bg backgroundColor]) {
            backgroundView = bg;
            break;
        }
    }

    if (backgroundView) {
        [backgroundView retain];
        [_backgroundViewPool removeObject:backgroundView];
    } else {
        backgroundView = [[UIView alloc] init];
        [backgroundView setBackgroundColor:color];
    }

    [cell setBackgroundView:[backgroundView autorelease]];
}

It works except when you scroll really fast. Some of the background views disappear! I suspect the background views are still being used in more than one cell, but I really don't know what's going on because the background views are supposed to be removed from the queue once it's reused making it impossible for the background view to be used in more than one visible cell.


I've been looking into this since I have posted this question. The current solutions for custom background views for grouped table view cells online are unsatisfactory, they don't used cached views. Additionally, I don't want to have use the solution proposed by XJones and jszumski because it's gonna get hairy once reusable custom cells (e.g., text field cell, switch cell, slider cell) are taken into account.

like image 813
Espresso Avatar asked Apr 13 '13 17:04

Espresso


1 Answers

Have you considered using 4 separate cell identifiers for the "lonely, "top", "bottom", and "middle" cases and setting the backgroundView only once when initializing the cell? Doing it that way lets you leverage UITableView's own caching and reuse without having to write an implementation on top of it.


Update: An implementation for a grouped UITableViewController subclass that reuses background views with a minimal number of cell reuse identifiers (Espresso's use case). tableView:willDisplayCell:forRowAtIndexPath: and tableView:didDisplayCell:forRowAtIndexPath: do the heavy lifting to apply or reclaim each background view, and the pooling logic is handled in backgroundViewForStyle:.

typedef NS_ENUM(NSInteger, JSCellBackgroundStyle) {
    JSCellBackgroundStyleTop = 0,
    JSCellBackgroundStyleMiddle,
    JSCellBackgroundStyleBottom,
    JSCellBackgroundStyleSolitary
};

@implementation JSMasterViewController {
    NSArray *backgroundViewPool;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // these mutable arrays will be indexed by JSCellBackgroundStyle values
    backgroundViewPool = @[[NSMutableArray array],  // for JSCellBackgroundStyleTop
                           [NSMutableArray array],  // for JSCellBackgroundStyleMiddle
                           [NSMutableArray array],  // for JSCellBackgroundStyleBottom
                           [NSMutableArray array]]; // for JSCellBackgroundStyleSolitary
}


#pragma mark - Table View

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 5;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (section == 2) {
        return 1;

    } else if (section == 3) {
        return 0;
    }

    return 5;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSInteger section = indexPath.section;
    NSInteger row = indexPath.row;

    static NSString *switchCellIdentifier = @"switchCell";
    static NSString *textFieldCellIdentifier = @"fieldCell";
    static NSString *textCellIdentifier = @"textCell";

    UITableViewCell *cell = nil;

    // apply a cached cell type (you would use your own logic to choose types of course)
    if (row % 3 == 0) {
        cell = [tableView dequeueReusableCellWithIdentifier:switchCellIdentifier];

        if (cell == nil) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:switchCellIdentifier];

            UISwitch *someSwitch = [[UISwitch alloc] init];
            cell.accessoryView = someSwitch;

            cell.textLabel.text = @"Switch Cell";
        }

    } else if (row % 3 == 1) {
        cell = [tableView dequeueReusableCellWithIdentifier:textFieldCellIdentifier];

        if (cell == nil) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:textFieldCellIdentifier];

            UITextField *someField = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 80, 30)];
            someField.borderStyle = UITextBorderStyleRoundedRect;
            cell.accessoryView = someField;

            cell.textLabel.text = @"Field Cell";
        }

    } else {
        cell = [tableView dequeueReusableCellWithIdentifier:textCellIdentifier];

        if (cell == nil) {
            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:textCellIdentifier];

            cell.textLabel.text = @"Generic Label Cell";
        }
    }

    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.textLabel.backgroundColor = [UIColor clearColor];
    cell.detailTextLabel.text = [NSString stringWithFormat:@"[%d, %d]", section, row];
    cell.detailTextLabel.backgroundColor = [UIColor clearColor];

    return cell;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    // apply a cached background view
    JSCellBackgroundStyle backgroundStyle = [self backgroundStyleForIndexPath:indexPath tableView:tableView];
    cell.backgroundView = [self backgroundViewForStyle:backgroundStyle];
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    JSCellBackgroundStyle backgroundStyle = [self backgroundStyleForIndexPath:indexPath tableView:tableView];
    NSMutableArray *stylePool = backgroundViewPool[backgroundStyle];

    // reclaim the background view for the reuse pool
    [cell.backgroundView removeFromSuperview];

            if (cell.backgroundView != nil) {
            [stylePool addObject:cell.backgroundView];
            }

    cell.backgroundView = nil; // omitting this line will cause some rows to appear without a background because they try to be in two superviews at once
}

- (JSCellBackgroundStyle)backgroundStyleForIndexPath:(NSIndexPath*)indexPath tableView:(UITableView*)tableView {
    NSInteger maxRow = MAX(0, [tableView numberOfRowsInSection:indexPath.section] - 1); // catch the case of a section with 0 rows

    if (maxRow == 0) {
        return JSCellBackgroundStyleSolitary;

    } else if (indexPath.row == 0) {
        return JSCellBackgroundStyleTop;

    } else if (indexPath.row == maxRow) {
        return JSCellBackgroundStyleBottom;

    } else {
        return JSCellBackgroundStyleMiddle;
    }
}

- (UIView*)backgroundViewForStyle:(JSCellBackgroundStyle)style {
    NSMutableArray *stylePool = backgroundViewPool[style];

    // if we have a reusable view available, remove it from the pool and return it
    if ([stylePool count] > 0) {
        UIView *reusableView = stylePool[0];
        [stylePool removeObject:reusableView];

        return reusableView;

    // if we don't have any reusable views, make a new one and return it
    } else {
        UIView *newView = [[UIView alloc] init];

        NSLog(@"Created a new view for style %i", style);

        switch (style) {
            case JSCellBackgroundStyleTop:
                newView.backgroundColor = [UIColor blueColor];
                break;

            case JSCellBackgroundStyleMiddle:
                newView.backgroundColor = [UIColor greenColor];
                break;

            case JSCellBackgroundStyleBottom:
                newView.backgroundColor = [UIColor yellowColor];
                break;

            case JSCellBackgroundStyleSolitary:
                newView.backgroundColor = [UIColor redColor];
                break;
        }

        return newView;
    }
}

@end

Although you could very easily get away with dumping all views into one reuse pool, it complicates some of the looping logic and this way is easier to comprehend.

like image 161
jszumski Avatar answered Oct 04 '22 01:10

jszumski