7

Does anyone have suggestions on how to purge a cached UITableViewCell?

I'd like to cache these cells with reuseIdentifier. However, there are times when I need to delete or modify some of the table rows. I expect to call reloadData after the row changes.

Right now, dequeueReusableCellWithIdentifier always returns the cached(obsolete) entry from before. How do I indicate that the cache is stale and needs to be purged?

JasonMArcher
  • 14,195
  • 22
  • 56
  • 52

8 Answers8

7

I'm not sure why you're trying to purge cells in the first place. Every time you dequeue a cell, you need to re-set any data that needs to be displayed. The caching just prevents you from having to set up any non-changing properties every time. But the actual data that's being displayed must be set, even if the cell was cached before.

Note that your reuse identifier is supposed to be the same for all the cells of the same type. If you're doing something silly like calculating the identifier based on the row in question, then you're doing it wrong.

Your code should look something like

- (UITableViewCell *)tableView:(UITableView *)view cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *identifier = @"CellIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        // no cached cell, create a new one here
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];
        // set any properties that all such cells should share, such as accessory, or text color
    }
    // set data for this particular cell
    cell.textLabel.text = @"foo";
    return cell;
}

In that example, note how I always set the data for the cell every single time, and all cells share the same identifier. If you follow this pattern, you should have no reason at all to try and "purge" your cells, as any old data will be overwritten anyway. If you have multiple types of cells, you may want to use multiple identifiers in that case, but it's still 1 identifier per cell type.

Lily Ballard
  • 182,031
  • 33
  • 381
  • 347
  • Using a different identifier for each row is an easy to trick to load remote thumbnails asynchronously on each table view cell. Otherwise, the cell could load the thumbnails that was being displayed in other rows that was loaded before. If you have a recommendation to handle this situation elegantly, I would like to know. – Jesse Armand Mar 01 '11 at 10:23
  • 2
    @Jesse Armand: Using a different identifier for each row is a great way to leak every single cell. Of course, these aren't "real" leaks, as they'll go away once the table itself does, but you are aggressively caching objects that will never be used again. You just need to be smarter about either cancelling the thumbnail load when the cell is reused, or checking that the cell still represents the same object when your thumbnail is ready to be displayed. – Lily Ballard Mar 01 '11 at 21:45
  • 2
    @Yar: If you're not sharing your identifier between cells, then you're intentionally "leaking" every cell you create (I put that in quotes because it's only leaked as long as the table is alive). And if you're not using `dequeue` then you're, again, "leaking" every cell you create. You're not saving *one* alloc/init, you're saving *EVERY SINGLE* alloc/init past the first 7 (or so, depending on cell height and table height, basically just `numCellsVisible + 2`). – Lily Ballard Nov 07 '11 at 08:32
  • Thanks @KevinBallard, sorry I deleted my comment but your response stands on its own. Thanks so much. I finally understand this topic and what I've been doing wrong all this time. – Dan Rosenstark Nov 07 '11 at 08:37
  • @KevinBallard you said that " if we follow this patter, you should have no reason at all to try and purge your cells, as any old data will be overwritten anyway", i am following this patter just as you have it, and when i add a cell, change its `textlabel` property, delete it and then add another cell that takes it place in the index path, it suddenly gets the same `textLabel`, why is this? Thanks! – iProRage Jan 25 '12 at 04:59
  • @KevinBallard sorry to revive this after 5 years: so you say "it's only leaked as long as the table is alive"... so the queued `UITableViewCell` instances get dumped from the queue (and can dealloc)... when the `UITableView` deallocs? Thanks! – Dan Rosenstark Apr 27 '16 at 16:14
  • 1
    @DanRosenstark That's correct. The `UITableView` queues the reusable cells, so once the `UITableView` deallocs, the queues are released and therefore all the cached cells are released too. – Lily Ballard Apr 28 '16 at 18:30
  • @KevinBallard that's too bad: were it not true, it could've solved a retain-cycle issue I have ;) Thanks as always man! – Dan Rosenstark Apr 28 '16 at 20:55
3

I don't know how to purge the cache, however, I use a workaround how to handle the situation, when the rows need to be changed.

First of all, I don't use static cell identifier. I ask the data object to generate it (pay attention to [myObject signature], it provides a unique string describing all needed properties):

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyObject *myObject = _dataSource[indexPath.row];
    NSString *cellId = [myObject signature];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
        [myObject configureCell:cell];
    }
    [myObject updateCellWithData:cell];
    return cell;
}

[myObject signature] provides different signatures for different list of properties. So, if my object is changed, I just call [self.tableView reloadData], myObject will provide a new signature, and the table load a new cell for it.

[myObject configureCell:cell] places all needed subviews to the cell.

[myObject updateCellWithData:cell] updates the subviews of the cell with current data.

Vladimir Obrizan
  • 2,538
  • 2
  • 18
  • 36
  • 1
    This is a great idea. Unfortunately it doesn't work with cells loaded from a XIB, since the identifier used in cellForRowAtIndexPath has to match the identifier set in the XIB. – MusiGenesis Jan 17 '14 at 15:16
3
while ([tableView dequeueReusableCellWithIdentifier:@"reuseid"]) {}
wihing
  • 541
  • 4
  • 9
  • This is a good solution. When you dequeue, you pass along a reuse id. If you change/increment the reuse id it will generate a new cell. Presumably, iOS will garbage collect old cells when/if it needs it to manage memory. – Mike M Aug 03 '16 at 12:29
2

I have found a really good method.

In .h

//add this
int reloadCells;

In .m

- (void)dumpCache {
    reloadCells = 0;
    [tableView reloadData];
}

-(void)tableView:(UITableView *)tableView cellForRowAtUndexPath:(NSIndexPath *)indexPath {
    static NSString *CellID = @"Cell"
    UITableViewCell *cell = [tableView dequeCellWithReuseIdentifier:CellID];
    if (cell == nil || reloadCells < 12) {
        cell = [[UITableViewCell alloc] initWithFormat:UITableViewCellStyleDefault reuseIdentifier:CellID];
        reloadCells ++;
    }
    cell.textLabel.text = @"My Cell";

return cell;
}
JasonMArcher
  • 14,195
  • 22
  • 56
  • 52
Jake Dahl
  • 171
  • 2
  • 3
  • 13
  • I don't think the person liked the constant "12." You have to pick a value larger than the maximum number of cells that will every need to be cached -- might be a guess. But, the solution is so simple, and it works!, so I'm giving it an upvote. – Jeff Apr 01 '13 at 00:03
  • How does `(void) dumpCache` be called in this scenario? – topLayoutGuide Jan 28 '14 at 14:16
  • You would call dumpCache when you need to re make all your cells. It would be called using [self dumpCache]; – Jake Dahl May 20 '14 at 04:37
1

I had to something similar today. I have a gallery of images, and when the user deletes them all, the gallery needs to get rid of the last queued cell and replace it with a "gallery empty" image. When the delete routine completes, it sets a 'purge' flag to YES, then does a [tableView reloadData]. The code in cellForRowAtIndexPath looks like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

    NSString *cellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifier];
    if (purge) {
        cell = nil;
        purge = NO;
    }
    if (cell == nil) {
        //*** Do usual cell stuff
    }
    return cell
}
mpemburn
  • 2,776
  • 1
  • 35
  • 41
  • This worked for me - the only difference was I had to run through all cells THEN set purge=NO. I added: if (([indexPath row]+1)==(listOfItems.count)) { self.purge=0; } right before return cell. – Jennifer Nov 11 '12 at 18:59
1

I am coding ObjC (manual reference counting) and found some strange behavior that prevents UITableView and UITableViewCells from getting released, although my memory management is correct. UITableView and UITableViewCell seem to retain each other. And yes, I found a way to force the UITableView and its cells to release each other (remove cached cells).

To do this, basically you ask a table view for a reusable cell. The table view will remove it from its reusable cells cache. After all you tell that cell to remove from its superview. Done.

Now as a stand-alone class... First, you need an array of all cell identifiers you used. Next, you implement a dummy data source and let that data source clear the UITableView by reloading itself with the "ClearAllCachedCells" Data source:

#import <UIKit/UIKit.h>
@interface ClearAllCachedUITableViewCellsDataSource : NSObject <UITableViewDataSource, UITableViewDelegate>

+(void)clearTableView:(UITableView*)tv
     reuseIdentifiers:(NSArray<NSString*>*)cellIdentifiers
  delegateAfterFinish:(id<UITableViewDelegate>)dg
dataSourceAfterFinish:(id<UITableViewDataSource>)ds;

@end

And the magic happens in the .m file:

#import "ClearAllCachedUITableViewCellsDataSource.h"


@interface ClearAllCachedUITableViewCellsDataSource () {
    BOOL clearing;
}
@property (nonatomic, readonly) UITableView *tv;
@property (nonatomic, readonly) NSArray<NSString*> *identifiers;
@property (nonatomic, readonly) id ds;
@property (nonatomic, readonly) id dg;
@end

@implementation ClearAllCachedUITableViewCellsDataSource

-(void)dealloc {
    NSLog(@"ClearAllCachedUITableViewCellsDataSource (%i) finished", (int)_tv);
    [_tv release];
    [_ds release];
    [_dg release];
    [_identifiers release];
    [super dealloc];
}

-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {
    [self performSelectorOnMainThread:@selector(clear) withObject:nil waitUntilDone:NO];
    NSLog(@"TV (%i): reloading with zero cells", (int)_tv);
    return 0;
}

-(void)clear {
    if (!clearing) {
        clearing = YES;
        for (NSString *ident in self.identifiers) {
            UITableViewCell *cell = [_tv dequeueReusableCellWithIdentifier:ident];
            while (cell) {
                NSLog(@"TV (%i): removing cached cell %@/%i", (int)_tv, ident, (int)cell);
                [cell removeFromSuperview];
                cell = [_tv dequeueReusableCellWithIdentifier:ident];
            }
        }
        self.tv.delegate = self.tv.delegate == self ? self.dg : self.tv.delegate;
        self.tv.dataSource = self.tv.dataSource == self ? self.ds : self.tv.dataSource;
        [self release];
    }
}

+(void)clearTableView:(UITableView*)tv
     reuseIdentifiers:(NSArray<NSString*>*)cellIdentifiers
  delegateAfterFinish:(id<UITableViewDelegate>)dg
dataSourceAfterFinish:(id<UITableViewDataSource>)ds {
    if (tv && cellIdentifiers) {
        NSLog(@"TV (%i): adding request to clear table view", (int)tv);
        ClearAllCachedUITableViewCellsDataSource *cds = [ClearAllCachedUITableViewCellsDataSource new];
        cds->_identifiers = [cellIdentifiers retain];
        cds->_dg = [dg retain];
        cds->_ds = [ds retain];
        cds->_tv = [tv retain];
        cds->clearing = NO;
        tv.dataSource = cds;
        tv.delegate = cds;
        [tv performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];
    }
}

@end
Anticro
  • 685
  • 4
  • 12
0

No way, I think... I used a completely different approach. Instead of relying on UITableView cache, I build my own. In this way I have perfect control on when and what to purge. Something like this...

given:

NSMutableDictionary *_reusableCells;
_reusableCells = [NSMutableDictionary dictionary];

I can do:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = @"whatever-you-need";

    UITableViewCell *cell = [_reusableCells objectForKey:cellIdentifier];

    if (cell == nil)
    {
        // create a new cell WITHOUT reuse-identifier !!
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
        // store it in my own cache
        [_reusableCells setObject:cell forKey:cellIdentifier];
        /* ...configure the cell... */
    }

    return cell;
}

and:

-(void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
    [_reusableCells removeAllObjects];
}

Hope this may help.

roberto.buratti
  • 2,487
  • 1
  • 16
  • 10
0

The old data from previous usage of the cell should be cleared with the message prepareForReuse. When a cell is dequeued this message is sent to the cell before it is returned from dequeueReusableCellWithIdentifier:.

Giao
  • 14,725
  • 2
  • 24
  • 18
  • OK, thanks. I was hoping to find a way to invalidate the whole cache in one swoop. However, this solution (manually clear the data to invalidate the cache and do the invalidation, one row at a time) should also work –  Feb 18 '10 at 06:56
  • 2
    I don't think you understand Giao's answer. He didn't suggest anything at all for you to do. The method `prepareForReuse:` is sent to every cell as it's being dequeued, before it's handed to you. This is something that happens automatically, and you only care about it if you're implementing your own subclass of `UITableViewCell`. – Lily Ballard Jan 05 '11 at 04:42