Clear tableView cell cache (or remove an entry) - iphone

I have the same question problem as described here How to purge a cached UITableViewCell
But my problem can't be solved with "resetting content".
To be precise - I use a custom cell (own class). While running the application it is possible that I have to use a different "cell type".
It's the same class - but it has (a lot of) differnt attributes.
Of course I could always reset all the things at "PrepareForReuse" but that's not a good idea I guess (there are a lot things to reset).
My idea - I do all these things in the constructor of the cell.
And all the rows will use this "type of cell" later.
When the (seldom) situation comes that I have to change the look of all rows I create a new instance of this kind of cell with different settings.
And now I want to replace the queued cell with this new one.
I tried it with simply calling the constructor with the same cellidentifier (in the hope it will replace the existing one) but that doesn't work.
I also didn't find a "ClearReusableCells" or something like this.
Is there a way to clear the cache - or to remove / replace a specific item?
Manfred

Create each cell type (even if they use the same cell class) using a different identifier. So if you have 2 cell types, define 2 identifiers and keep them separate.
I'm not sure where your problem is. You have a bunch of cells, with appearance A, then the user takes some action and they need to become appearance B. If you call reloadData or one of the more granular methods, your datasource will be called again for cellForRowAtIndexPath. Just implement this method to segregate the two cell types.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString* identifier = which mode are we in
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:identifier]; // Will return nil if we haven't got this cell
if( !cell ) {
// Create different cell type based on the identifier
}
return cell;
}

Update
This doesn't work. The memory for the cell that is returned from dequeueReusableCellWithIdentifier is never released.
If you initialize a cell with a non-nil reuseIdentifier, then that cell will NOT be freed until the tableView itself is released. This is true even if [[tableView valueForKey:#"reusableTableCells"] removeAllObjects] is called. Unfortunately, the tableView is retaining the cell in some other private member and the only way to free it is by destroying the tableView.
Short Answer
The answer to the title of the question of how to clear the tableView cell cache is that Apple does not provide the "ClearReusableCells" functionality, but it is reasonably easy to implement yourself. Simply track the time that the tableView's cache was last cleared and track the time that the cell was created. A cell is considered to be dirty if it was created before the tableView's last cache refresh time.
Alternatives
Steven Canfield's answer - This is a good answer to the specific question, but it does not scale well if there are multiple attributes that can be changed independently of each other. In that case, you can very quickly have lots of 'modes' and that can get unmanageable. Also, with respect to memory usage, it is true that Apple code is responsible for managing the memory, but it is also true that library cannot know when a particular identifier might be needed again, so it will likely end up keeping these cached cells around longer than it needs to.
Recreate the tableView - This is a brute force mechanism that is guaranteed to clear the cell cache. Unfortunately, it is usually inconvenient to recreate the tableView.
Jake's Dahl's answer from the alternate question - This is a good mechanism for clearing the cache that probably worked at the time it was written, but it relies on some implementations that Apple does not guarantee, and that have, in fact, already changed.
Implementation
There are multiple ways of tracking the tableView's cache refresh time and the cell creation time. I've shown a mechanism below that uses subclasses. Of course, for this to work, you need to make sure that the code that instantiates the table and cells instantiates the subclasses.
#interface MyTableView : UITableView
#property(nonatomic,assign) NSTimeInterval cellCacheRefreshTime ;
#end
#interface MyTableViewCell : UITableViewCell
#property(nonatomic,assign) NSTimeInterval creationTime ;
#end
#implemetation MyTableView
-(void) refreshCellCache {
self.cellCacheRefreshTime = [NSDate timeIntervalSinceReferenceDate];
}
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier {
MyTableViewCell *cell = (id)[super dequeueReusableCellWithIdentifier:identifier] ;
if( cell.creationTime < aTableView.cellCacheRefreshTime ) {
return nil ;
}
return cell ;
}
#end
#implemetation MyTableViewCell
-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier] ;
self.creationTime = [NSDate timeIntervalSinceReferenceDate];
return self ;
}
#end

Related

Best practice in UITableView with different kind of UITableViewCell

her's what we need to do:
the data source of the tableView core data.
a tableView that contain 5 different type of custom tableViewCell.
every tableViewCell is completely different from others.
some tableViewCell will have a progressBar.
we have 3 solutions:
use a unique tableViewCell with a unique reuse identifier.
use a unique tableViewCell with 5 reuse identifier.
use 5 tableViewCell and 5 reuse identifier.
we test the 1st solution, it's ok in iphone 5/ iphone 4S, but in iphone 4 it's slooooooow (and we need to support the 3GS too ...).
The question: which solution will be better? or if you have other solution it will be great.
the favor: can you explain how the reuse identifier work (in details please :) ), like when the first cells are allocated, when they are reused (with 1 and with different reuse identifier), when they are desallocated ... ?
thank you.
If all cells are completely different (layout) you should init each cell type with a unique reuse identifier to benefit from the performance advantages of dequeing cells later. The tableview will init as many cells as necessary (depending on the number of sections and rows in each section) for filling its bounds no matter which reuse identifier you assign. Cells are cached as they disappear from the visible area of the tableview. That means at least one cell of each reuse identifier that has gone off the screen is kept in memory for reuse. When the tableview is scrolled and another row is needed it will ask cellForRowAtIndexPath to provide a cell for this row. When there's no cell with a specified resuse identifier in the queue a new cell is created, subviews are initialized and layout / arrangement of subviews is done. If there's a cell with the specified resuse identifier in cache the tableview takes this cell "as is" and customizes it just according to the specifications you provide in cellForRowAtIndexPath like assign a different image to an imageView or set a label's text which is much more "inexpensive" than creating a completely new cell. You can check that by only setting a label's text in your custom cell's initWithStyle. If you don't modify the text after calling dequeueReusableCellWithIdentifier in cellForRowAtIndexPath the label's text will be the same in every cell dequeued with the same reuse identifier. Also complex backgrounds (e.g. with gradients) will be reused and don't need to be redrawn everytime a new cell appears on the screen. Assigning ONE reuse identifier to all different types of cells, reusing the cells would cause nearly the same effort as creating a new cell for each row (assuming each cell type equally spread). Cells in the queue will be deallocated when the tableview is deallocated. Hope this helps you understanding the concept of reusing tableview cells.
This was my solution, and it works ok on 3gs, now depends how complex is your cell and how many things you do # [cell load] method. Try to avoid for/while loops there.
if(indexPath.row == 0){
static NSString *CellIdentifier = #"HeadCell";
kHeaderCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[cell load];
return cell;
}
if(indexPath.row == 1){
static NSString *CellIdentifier = #"HistCell";
kHisoryCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[cell load];
return cell;
}
...and so on

When do custom table view cells dealloc?

I was under the impression that table view cells never got dealloced until the app crash because you are able to resuse them. But when I was profiling my table view, I realized that something was calling dealloc on my custom cell. What exactly dealloc's custom cells and can I stop it?
Using the common "reuse" pattern:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellId = #"Foo";
UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:cellId];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId]
autorelease];
}
// update the cell content
return cell;
}
The table view creates as many cells as required to fill the its frame height.
When disappearing from screen they are removed from the view and put in the reuse queue, thus not deallocated.
All the cells are dealloc'd when the tableView is dealloc'd and some cells may be dealloc'd when you number of rows changes (say you had 20 cells before and only 2 after update).
You might be tempted to get rid of cells reuse but you would lose all the magic done behind to keep a low memory footprint and to have a smooth scrolling experience.
The semantics of the dequeueReusableCellWithIdentifier: selector are opaque (as far as I know). If you want complete control of your table view cells, don't use that selector to get a cell: just construct a new one or use your own pool for reuse.
Table views create and shed cells as needed. This is to allow for very long lists to be scrolled through without storing them all in memory. A virtually infinite scrollable list is possible. A convenient way to retain your custom cells for the table view's lifespan is to add them to a mutable array upon creation. Before creating, look in the array to see if it already exists.

How to tell a UITableView to preload all Rows?

Is there a way to tell a UITableView to preload all rows?
The tableView is supposed to show several comments (up to 80 comments).
So my CommentCell uses a Setter to adapt the cell to a specific comment.
-(void)setComment:(Comment *)newComment {
if (newComment != comment) {
[comment release];
comment = [newComment retain];
/*
* set the cells view variables here
*/
}
}
This specific setter takes quite a bunch of processing resources and scrolling gets kinda laggy.
I am using a comment-specific reuseIdentifier instead of a static cellIdentifier when calling
dequeueReusableCellWithIdentifier:
in order to assure, that "newComment" equals the old "comment".
And in fact this does work great when scrolling over cells which have already been loaded.
But when scrolling through the comments for the first time, it still lags like hell.
Which leads me to my question:
Is there a way to tell the tableview to preload all cells? (which I doubt)
or
Do I have to implement my own cache instead of relying on "dequeueReusableCellWithIdentifier:"?
Keep in mind that your comment specific reuseIdentifier could be what is causing everything to go slow (or at least, it isn't helping). The reason we use reuseIdentifier for UITableViewCells is because if you try to allocate a new cell every time you need one it isn't as performant as if you can just reuse one that was already made.
I'd recommend pre-computing your comments so you can just set properties of your cells and reusing cells after they scroll off the tableview.
No, you have to preload your data in your dataSource. Just put everything you need in an array and fill the table's cells from that array of preloaded objects.

iPhone - What are reuseIdentifiers (UITableViewCell)?

From the official documentation:
The reuse identifier is associated with a UITableViewCell object that the table-view’s delegate creates with the intent to reuse it as the basis (for performance reasons) for multiple rows of a table view. It is assigned to the cell object in initWithFrame:reuseIdentifier: and cannot be changed thereafter. A UITableView object maintains a queue (or list) of the currently reusable cells, each with its own reuse identifier, and makes them available to the delegate in the dequeueReusableCellWithIdentifier: method.
http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UITableViewCell_Class/Reference/Reference.html#//apple_ref/occ/instp/UITableViewCell/reuseIdentifier
I don't understand this. Well, I understand the basic idea, I think, that you create UITableViewCells, and try to reuse as many as you can instead of making new ones (or something like that). But what exactly decides whether or not a cell is reusable? If I've got two identical (visually) cells, but with different texts (well I suppose they aren't entirely identical), can they both have the same identifier? Or should they have different ones? Or in what situation are you supposed to use different identifiers?
Can anyone clarify or link to a place where it is?
Ok, this is how I believe it works:
Using dequeueReusableCellWithIdentifier for the tableView, you can greatly speed things up. Instead of instantiating a lot of cells, you just instantiate as many as needed, i.e. as many that are visible (this is handled automatically). If scrolling to an area in the list where there are "cells" that haven't got their visual representation yet, instead of instantiating new ones, you reuse already existing ones.
You can try this yourself by doing this:
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
NSLog(#"new one");
}
else
{
NSLog(#"old one");
}
Remember, you only want dequeueReusableCellWithIdentifier to return a cell if it is applicable. So if a cell is going to be reused, make sure it is correct for the situation. That's what reuseIdentifiers are for. Usually, you will only need one. But there might be a list that uses several different kinds of cells, and in that case, you'd have to keep them separate by providing different reuseIdentifiers. Otherwise you might end up getting a cell that you treat as some other kind of cell (for example, UITableView cell instead of the custom one you wanted).
So basically, as I understand it, use different reuseIdentifiers for different kinds of cells, where kind means class. If you only use standard cells, you probably only need one reuseIdentifier.
This design pattern is known as object pooling.
Just to add some things to quano's otherwise very good answer: (I tried to add this as a comment, but it was too long!)
Even reuse identifiers can be omitted in developing, although this must be done in very specific circumstances. If you have a table view of 6-7 cells, and each one is different, you may find that creating a new cell with nil as the identifier may be preferable.
Having a reusable cell means that in each time the cellForRowAtIndexPath is called, you must check the cell, initialize it if there is no reusable cell, and outside of the init scope you must explicitly iterate through all possible indexpaths and set the values for each label explicitly depending on what kind of cell you have! So, in a table view with 10 dinstinct cells, you will have to take care of creating the cell if nil, and filling it up depending on what you created.
Therefore, in this case, it's preferable in terms of code maintenance to initialize each cell with nil identifier (since it's not going to be reused anyway) and fill each cell's info appropriately without worrying about reusing it.
UITableView is like having a cell pool for each reuseIdentifier, so that it recycle the cell
I like this video from http://oleb.net/blog/2014/05/scrollviews-inside-scrollviews/
http://im.ezgif.com/tmp/ezgif-3302899694.gif

Reference from UITableViewCell to parent UITableView?

Is there any way to access the owning UITableView from within a UITableViewCell?
Store a weak reference to the tableView in the cell, which you'd set in -tableView:cellForRowAtIndexPath: of your table's dataSource.
This is better than relying on self.superview to always be exactly the tableView is fragile. Who knows how Apple might re-organize the view hierarchy of UITableView in the future.
Here's a nicer way to do it, which does not rely on any particular UITableView hierarchy. It will work with any future iOS version, provided that UITableView does not change classname altogether. Not only this is extremely unlikely, but if it does happen you will have to retouch your code anyway.
Just import the category below and get your reference with [myCell parentTableView]
#implementation UIView (FindUITableView)
-(UITableView *) parentTableView {
// iterate up the view hierarchy to find the table containing this cell/view
UIView *aView = self.superview;
while(aView != nil) {
if([aView isKindOfClass:[UITableView class]]) {
return (UITableView *)aView;
}
aView = aView.superview;
}
return nil; // this view is not within a tableView
}
#end
// To use it, just import the category and invoke it like so:
UITableView *myTable = [myTableCell parentTableView];
// It can also be used from any subview within a cell, from example
// if you have a UILabel within your cell, you can also do:
UITableView *myTable = [myCellLabel parentTableView];
// NOTE:
// If you invoke this on a cell that is not part of a UITableView yet
// (i.e., on a cell that you just created with [[MyCell alloc] init]),
// then you will obviously get nil in return. You need to invoke this on cells/subviews
// that are already part of a UITableView.
UPDATE
There is some discussion in the comments about whether keeping a weak reference is a better approach. It depends on your circumstances. Traversing the view hierarchy has some small runtime penalty as you are looping until the target UIView is identified. How deep are your views? On the other hand, keeping a reference on every cell has a minimal memory penalty (a weak reference is a pointer after all), and generally adding object relationships where they are not needed is considered a bad OO design practice for many reasons, and should be avoided (see details in the comments below).
More importantly, keeping table references inside cells adds code complexity and can lead to errors, because UITableViewCells are reusable. It is no coincidence that UIKit does not include a cell.parentTable property. If you define your own you must add code to manage it, and if you fail to do so effectively you can introduce memory leaks (i.e., cells live past the lifetime of their table).
Because typically you'll be using the category above when a user interacts with a cell (execute for a single cell), and not when laying-out the table in [tableView:cellForRowAtIndexPath:] (execute for all visible cells), the runtime cost should be insignificant.
Xcode 7 beta, Swift 2.0
This works fine for me, in my opinion it has nothing to do with the hierarchy or whatever. I had no trouble with this approach so far. I've used this for many async callbacks (ex. when an API request is done).
TableViewCell class
class ItemCell: UITableViewCell {
var updateCallback : ((updateList: Bool)-> Void)? //add this extra var
#IBAction func btnDelete_Click(sender: AnyObject) {
let localStorage = LocalStorage()
if let description = lblItemDescription.text
{
//I delete it here, but could be done at other class as well.
localStorage.DeleteItem(description)
}
updateCallback?(updateList : true)
}
}
Inside table view class that implements the DataSource and Delegate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: ItemCell = self.ItemTableView.dequeueReusableCellWithIdentifier("ItemCell") as! ItemCell!
cell.updateCallback = UpdateCallback //add this extra line
cell.lblItemDescription?.text = self.SomeList[indexPath.row].Description
return cell
}
func UpdateCallback(updateTable : Bool) //add this extra method
{
licensePlatesList = localStorage.LoadNotificationPlates()
LicenseTableView.reloadData()
}
Ofcourse you can put any variable in the updateCallback and change it's function in the tableView accordingly.
Someone might want to tell me if it is save to use though, just to be sure.
You have to add a reference back to the UITableView when you construct the table view cell.
However, almost certainly what you really want is a reference to your UITableViewController... that requires the same thing, set it as a delegate of the cell when you build the cell and hand it to the table view.
An alternate approach if you are wiring up actions is to build the cells in IB, with the table view controller as the files owner - then wire up buttons in the cell to actions in the table view controller. When you load the cell xib with loadNibNamed, pass in the view controller as the owner and the button actions will be wired back to the table view controller.
If you have custom classes for your UITableViewCells, you can add an id type variable in your cell's header, and synthesize the variable. After you set the variable when you load the cell, you are free to do what you please with the tableview or any other higher view without much hassle or overhead.
cell.h
// interface
id root;
// propery
#property (nonatomic, retain) id root;
cell.m
#synthesize root;
tableviewcontroller.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// blah blah, traditional cell declaration
// but before return cell;
cell.root = tableView;
}
Now you can call any of the tableview's methods from within your cell using the root variable. (e.g., [root reloadData]);
Ah, takes me back to the good old days of flash programming.
The two methods in other answers are: (A) store a reference to the table, or (B) walk up the superviews.
I'd always use something like (A) for model objects and (B) for table cells.
Cells
If you are dealing with a UITableViewCell, then AFAIK you must either have the UITableView at hand (say you are in a table delegate method), or are dealing with a visible cell that is in the view hierarchy. Otherwise, you may well be doing something wrong (please note the "may well").
Cells are liberally reused and if you happen to have one that is not visible then the only real reason that cell exists is because of iOS UITableView performance optimization (a slower iOS version would have released and hopefully dealloc'd the cell when it moved off screen) or because you have a specific reference to it.
I guess this is probably the reason that table cells are not endowed with a tableView instance method.
So (B) gives the right result for all iOS's so far, and all future ones until they radically change how views work.
Though in order to avoid writing generalizable code over and over, I'd use this:
+ (id)enclosingViewOfView:(UIView *)view withClass:(Class)returnKindOfClass {
while (view&&![view isKindOfClass:returnKindOfClass]) view=view.superview;
return(view);
}
and a convenience method:
+ (UITableView *)tableForCell:(UITableViewCell *)cell {
return([self enclosingViewOfView:cell.superview withClass:UITableView.class]);
}
(or categories if you like)
BTW, if you are concerned about the effect of a loop with 20 or so iterations of that size on your app performance,.. don't.
Models
If you are talking about the model object that is displayed in the cell, then definitely that model could/should know about its parent model, which may be used to find, or trigger changes in, the table(s) that the cell's model might be displayed in.
This is like (A), but less brittle with future iOS updates (eg one day they might make the UITableViewCell reuse cache exist per reuseidentifier, rather than per reuseidentifier per tableview, on that day all the implementations that use the weak reference method will break).
Th model method would be used for changes to the data displayed in the cell (i.e. model changes) since changes will propagate wherever the model is displayed (eg. some other UIViewController somewhere else in the app, logging, ...)
The cell method would be used for tableview actions, which would likely always be a bad idea if the cell isn't even a subview of a table (though it's your code, go nuts).
Either way, use a unit test rather than assuming that seemingly cleaner code just works when they update iOS.
UITableView *tv = (UITableView *) self.superview.superview;
UITableViewController *vc = (UITableViewController *) tv.dataSource;