Apple's iOS TableView and cell reuse is killing me. I searched and searched and studied, but can't find good docs or good answers. The problem is that when the TableView reuses cells things like Checkmarks (cell accessory) set on a selected Cell are repeated in the cells further down in the table view. I understand that cell reuse is by design, due to memory constraints, but if you have a list with say 50 items, and it starts setting extra checkmarks where they're not wanted, this makes whole endeavor useless.
All I want to do is set a checkmark on a cell I've selected. I've tried this using my own custom cell class, and standard cells generated by a boiler plate TableView class, but it always ends up the same.
Apple even have an example project called TouchCell you can download from the dev center, that is supposed to show a different way of setting a checkmark using a custom cell with an image control on the left. The project uses a dictionary object for a data source instead of a muteable array, so for each item there is a string value and bool checked value. This bool checked value is supposed to set the checkmark so it can track selected items. This sample project also displays this goofy behavior as soon as you populate the TableView with 15+ cells. The reuse of cells starts setting unwanted check marks.
I've even tried experimenting with using a truely unique Cell Identifier for each cell. So instead of each cell having something like #"Acell" I used a static int, cast to a string so the cells got #"cell1", #"cell2" etc. During testing though, I could see that hundreds of new cells where generated during scrolling, even if the table only had 30 items.
It did fix the checkmark repeat problem, but I suspect the memory usage was going way too high.
It's as though the cells that are not currently in the viewable area of the table are created all over again when they are scrolled back into view.
Has anyone come up with an elegant solution to this irritating behavior?
cell reusing can be tricky but you have to keep 2 things in mind:
Use one identifier for one type of cell - Using multiple identifiers is really only needed when you use different UITableViewCell-subclasses in one table view and you have to rely on their different behaviour for different cells
The cell you reuse can be in any state, which means you have to configure every aspect of the cell again - especially checkmars / images / text / accessoryViews / accessoryTypes and more
What you need to do is to create a storage for your checkmark states - a simple array containing bools (or NSArray containing boolean NSNumber objects respectively) should do it. Then when you have to create/reuse a cell use following logic:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *reuseIdentifier = #"MyCellType";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
if(cell == nil) {
/* create cell here */
}
// Configure cell now
cell.textLabel.text = #"Cell text"; // load from datasource
if([[stateArray objectAtIndex:indexPath.row] boolValue]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
then you will have to react on taps:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[stateArray replaceObjectAtIndex:indexPath.row withObject:[NSNumber numberWithBool:![[stateArray objectAtIndex:indexPath.row] boolValue]]];
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
Just remember to use NSMutableArray for your data store ;)
Related
I am asking very basic doubt belongs to Tableview, I created tableview programmatically without Storyboard/Xib.
The tableView numberOfRowsInSection it will return 14, the cell view fully dynamic and each cell height will different from one another.
My questaion is in tableview delegate method
static NSString *cellIdentifier = #"Cell";
UITableViewCell *cell=(UITableViewCell *)[tableview dequeueReusableCellWithIdentifier:cellIdentifer];
if (cell == nil )
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifer];
// how many time entering this loop
}
// ( adding subview to cell view).
cell==nil means need to enter loop right. Depending on what parameter cell object become nil? how many times it will enter, is it once? not at all.
When I checked, it entered 6 times.
if I use cellIdentifier, it will enter 14 times because Identifier different and every time it will create space for cell, its right because each time name will different and while scrolling it will reused.
NSString *cellIdentifer= [NSString stringWithFormat:#"%ld,%ld",(long)indexPath.section,(long)indexPath.row];
which basis it will enter 6 times. why not one's or 14 times. Please suggest what I did wrong. Because if used #"Cell" identifier, while scrolling repeatedly view will overlap. If I used second one cell view object will not overlaps & looks like perfect, but device memory size will increase
ref by https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewCell_Class/index.html
If I right got your question you need register your class in viewDidLoad method or loadView where you created a tableview like that [tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier]; for right reuse in tableview
Code : 1
static NSString *cellIdentifier = #"Cell";
UITableViewCell *cell=(UITableViewCell *)[tableview dequeueReusableCellWithIdentifier:cellIdentifer];
if (cell == nil ){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifer];
// how many time entering this loop
}
In above code the loop will execute as number of visible rows in your tableview at first time. After that it will reuse cell as the cellIdentifier is same Cell and you need to update cell data as per indexpath.
it means if your table display 6 rows then it will execute for 6 times. Change rowheight and you can check.
Code : 2
NSString *cellIdentifer= [NSString stringWithFormat:#"%ld,%ld",(long)indexPath.section,(long)indexPath.row];
for above code the loop will execute total number of rows you have declared, Because it will create new cell for each indexpath.
If we give same Identifier to all cells, Disappearing cell uses the memory of Appearing cell. But, If we give different Identifier then every cell will have its own memory location and shows data perfectly.
Now suppose we have 1000 or more records to load in Table-view. If we will give different Identifiers, there will be lots of allocations in memory. This is the benefit of re-using cells.
Why 6 times ?
Because, if you give same identifiers, table will re-use cells. Maximum number of cells visible at the moment, are allocated at first. Then on scroll, appearing cell uses a memory location of a disappearing cell (cell dequeuing). So, every time you scroll, new cells are not allocated. Instead, already allocated cells are re-used
Why 14 times ?
Because, every cell has different identifier in this case. Every cell will have its own memory location.
Remember
Add subviews inside cell nil condition. Cell specific content should be assigned outside nil condition. Have a look at following code snippet:
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier = #"MY_CELL";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
/* Everything that is similar in all cells should be defined here
like background colors, label colors, indentation etc. */
}
/* Everything that is row specific should go here
like label text, progress view progress etc. */
return cell;
}
With same identifier:
When table cell is going to disappeared then this cell will be added in stack and these are reusable.
Now when we are going to show a cell then:
If we are using same identifier then controller will check that cell is available in stack with same identifier.
If yes, then we will get a table cell which is already used and UI was already set for this cell. So we need to reset UI before using it.
If not, then it will create new cell and trying to use it.
In your case I think 4-5 table cell is visible at a time so it is creating 6 table cell and reusing those cell.
With different identifier:
Table cell cell will not available in stack at creating cell for different indexpath. So it will create new one. so cell method will be called 14 times.
I have a list of custom cells in my tableview and as I scroll everything appears to fine and the cells appear to be in the same order. I have some functionality with my cells - as i select a cell (and it dynamically expands)the background color changes and a few other custom cell properties. Once I do this and then I start scrolling, different cells that i haven't even touched before show up, selected(expanded) and the cell only updates when I select it manually to the correct data. I seem to see duplicates and all kinds of craziness.
I know there are LOTS of posts about this on here so far but for me, so far nothing has worked. Would like some input on what I could do to stop this ridiculous behavior.
I have posted some code to give you a better idea of what I am doing. I know that 'dequeueReusableCellWithIdentifier' is the culprit but don't know of an alternative.
As side notes, this is a tableview(its own xib) that is a child view of a large view (also a xib). I have also already registered the nib for the tableview.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCustomCell *cell = [tableView dequeueReusableCellWithIdentifier:myIndentifier forIndexPath:indexPath];
if(self.currentSelectedIndex){
if(self.previousSelectedIndex){
//collapse cell
//configure cell in method(change background color etc)
}
else{
//expand cell
//configure cell in method(change background color etc)
}
}
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.currentSelectedIndex = indexPath;
[tableView beginUpdates];
if(self.currentSelectedIndex){
if(self.previousSelectedIndex && (self.previousSelectedIndex != self.currentSelectedIndex)){
[tableView reloadRowsAtIndexPaths:#[self.currentSelectedIndex, self.previousSelectedIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else{
[tableView reloadRowsAtIndexPaths:#[self.currentSelectedIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
[tableView endUpdates];
if(self.previousSelectedIndex == self.currentSelectedIndex){
self.previousSelectedIndex = nil;
}
else{
self.previousSelectedIndex = self.currentSelectedIndex;
}
}
What can I do or how would i make sure that nothing else in the list 'seems' to be selected(expanded) or prevent from appearing to see duplicates as i scroll? I already keep track of my current and last selected index(as shown in the code) so I suppose that I could use that somehow?
Dequeued Cells are Reused
Know that cells are re-used, so that the appearance of a UITableViewCell is persistent for the entire life of that cell.
This means that if you do not explicitly reset all the presentation view of your cell, and just returning it unchanged in cellForRowAtIndexPath, what you are returning may be a currently selected (or deselected) cached cell.
A possible location to reset a table cell is prepareForReuse.
Design note:
How are you maintaining self.currentSelectedIndex and self.previousSelectedIndex? This is typically quite dangerous, since you are attempting to replicate the UITableView behavior. It is for example, unlikely to work with multiple selection. Setting an active selection is unlikely handle situations when the OS didDeselectRowAtIndexPath, as a result of a keyboard dismissal for example.
I have been searching and reading all over but couldn't find any conclusive method to achieve what I want to and hope to find help here...
I have a UITableView which allows the user to add multiple Flavours and Percentages to a Recipe. I have implemented the method to add or delete rows of Flavours with a custom Cell / Nib and it works perfectly well.
The issue I'm facing now, is how to retrieve the values the user has provided per added row.
(Edit for Clarity: My problem is not the populating of data, but only the dynamic reading of all data so I can save it)
I do manage to get the values for the visible rows (I do understand how the Reuseidentifier and the Tableview works, per se that for memory management's sake, iOS only keeps track of the visible rows), but not the hidden ones.
I assume in theory that I have to create an Array of Cells outside of 'cellForRowAtIndexPath' which maintains all cells. But then I'm facing another conceptual problem that my custom Nib / cell doesn't show.... basically:
How can I then use / register a nib without using the dequeingidentifier
Or in General, how can I solve the overall problem to be able an read all user entered values per row
Here the code I'm using within my cellForRowAtIndexPath. As mentioned adding and remove cell works like a charm, that isn't the issue...
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecipeFlavourTableViewCell *cell;
int section = (int)indexPath.section;
if(section==0)
return [super tableView:tableView cellForRowAtIndexPath:indexPath];
if(!cell){
[tableView registerNib:[UINib nibWithNibName:#"RecipeFlavourCell" bundle:nil] forCellReuseIdentifier:#"Cell"];
cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
I have seen some Libraries doing it (e.g. XLForm) but do not understand (also when checking their sources) how they iterate through the values and overcome this dequeuing problem...
Any help is highly appreciated
EDIT 2: here the code I'm using to iterate through the cells in order to save the data, but as said I can only iterate through the visible cells:
- (IBAction)saveRecipe:(id)sender {
NSInteger dynamicRows = [self.tableView numberOfRowsInSection:1];
for (int i=0; i<dynamicRows; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:1];
RecipeFlavourTableViewCell *cell = (RecipeFlavourTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
NSLog(cell.flavour.text);
}
}
After 2 days of searching I finally came up with a solid solution. In case someone bumps into the same problem of dynamic forms with a tableview, here the solution:
As we understand, what ever cell is created in cellForRowAtIndexPath, it only persists as long as it is displayed. As soon as you scroll and the cell disappears, it gets automatically thrown out of memory. This behaviour makes it impossible to iterate through all cells at a later stages.
The steps to follow in order to make it work are as follows:
Preparation
Create an NSObject with all properties you want to persist in one form cell (-> cellObject)
In the ViewDidLoad of your controller create a NSMutableArray which will contain the cellObjects (-cellsArray)
Add as many cellObjects to the cellsArray as you initially want to appear in the Tableview
In numberOfRowsInSection return the count of you cellsArray
In the cellForRowAtIndexPath build your cells as usual BUT add a Textfield Delegate (self) to every Textfield in a cell
TextField Delegate
Implement:
- (void)textFieldDidEndEditing:(UITextField *)textField
and update your cellsArray Objects every time a Textfield ends editing. Per se, get the cellObject for the row and edit the properties with the value of the TextField
Add Row
When ever you add a row, just add an empty cellObject to your cellsArray and use the beginUpdates / insertRowsAtIndexPaths / endUpdates on your tableView (NOT reloadData as the already typed in data would get lost). Also add the following at the very beginning of your addRow method, as you want to make sure that if the user adds a row while editing a textfield, the latter gets persisted as well:
[self.view.window endEditing: YES];
Remove Row
Same as Add Row just reverse, remove the cellObject from your cellsArray and use deleteRowsAtIndexPaths on your tableView
Save Data
Now comes the trick: since you ought to always persist your data when a field ends editing mode, there is one case you need to cover: What if the user pushes "Save" when the focus is set on one TextField? Well at the very beginning of your Save Action insert the following:
[self.view.window endEditing: YES];
This make sure the the textFieldEndEditing will be triggered one last time for the current textField and that its data will also be persisted.
Finally iterate through your cellsArray and do whatever you want with it (validate, save etc)...
That's it, hope this can help anyone else as I couldn't find any valuable explanation anywhere else...
Lets assume that you have an NSArray and that it contains data you want to show. Your code should look something like this:
// Add this property to the class and fill it in with data you want to show
#property NSArray flavourElements;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RecipeFlavourTableViewCell *cell;
int section = (int)indexPath.section;
if(section==0)
return [super tableView:tableView cellForRowAtIndexPath:indexPath];
if(!cell){
[tableView registerNib:[UINib nibWithNibName:#"RecipeFlavourCell" bundle:nil] forCellReuseIdentifier:#"Cell"];
cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
// At this point your cell is ready for showing
// And you can change values in it by getting element from array that contains data
cell.flavorTextField = flavourElements[indexPath.row].flavour
cell.precentageTextField = flavourElements[indexPath.row].precentage
}
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
I wrote a code for accessing elements without knowing what you actually have, so you will need to adjust it a little bit to fit your app.
I have a uitableview with each cell having a scroll view as the subview.
the scrollview has a bunch of images in it.
so when i change the data in the data source and after calling the reload table
the images doesn't change but when i remove the dequeue the new data is reloaded.
is there any method to remove the contents in the dequeue so that i don't get the old data
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"looser"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
scrollview=[[myscrollView alloc]initwitharray:imagearray];
[cell.contentView addSubview:scrollview];
}
}
A tableview works as follows:
It has room for a certain amount of cells on the screen, let's say 7 as an example. The tableview will ask you for the 7 cells of indexes 0 through 6.
If the top cell leaves the screen by scrolling, it will be placed in the reusable cell queue. There are now 6 cells on the tableview.
A new one comes up at the bottom now, the tableview asks for the cell at index 7. You call dequeueReusableCell, and you get the one that was at the top earlier.
The tableView has no idea what your cell is like, as it can be subclassed, so it will not make any changes to it. It is up to you to use your knowledge of how the tablecell is constructed to empty it, then fill it with the correct new data.
The reason tableview works like this is for performance. In stead of having maybe 100 views that would have to be checked (or mostly, ignored, which also costs time) for every scroll movement, it has a maximum of 7.
So in short, no. There are no default methods to remove data from reusable cells in UITableView, since UITableView can not and should not know what kind of cells they are. It is up to you to clear the cells when the tableview gives them to you.
Create a custom cell and it generates a method
- (void) prepareForReuse{}
Which do you cleanse all data from a cell and the output will be an empty cell.
No, not while the cell is in the cache. When you dequeue a reusable cell you should clear out the old data first before using it again.
Maybe you should just remove the stuff you don't want.
I have a UITableViewController with UITextfield inside the tableview cells. If I scroll the table view, the user entered data in the textfields disappears. I tried to add the textfield data to a NSMutableArray but it still didn't work. Any help please.
When cellForRowAtIndexPath: is called, the cell you return has to be completely filled in with whatever data you want to show. So, if the cell includes a UITextfield, you'll need to set it's text property to the right value for that row in your data.
When a table cell disappears off the top or bottom of the screen, the UITableViewCell itself becomes available for re-use. (As you scroll, cells disappear, and new cells appear, but the UITableView class is re-using the UITableViewCell objects.) In cellForRowAtIndexPath: when you get a cached cell to use, you have to be sure to setup everything you want it to show for the row in question, otherwise you might see some odd behavior in your table.
Does this help?
EDIT:
Here's an example of the typical pattern used in cellForRowAtIndexPath:. Notice the use of dequeueReusableCellWithIdentifier:. That method returns a previously allocated but not in use UITableViewCell, if there is one. Notice further that if no cached cell is returned, the code creates a new one, and sets it up (with stuff that is independent of anything that might be row specific). Following that, you'd setup the cell as you need it for the row in question.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *SearchResultsCellIdentifier = #"SearchResultsCellIdentifer";
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:SearchResultsCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
reuseIdentifier:SearchResultsCellIdentifier] autorelease];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
// Your row-specific setup of the cell here
// ...
return cell;
}
Check the docs for specifics about these methods. There are LOTS of examples from Apple and elsewhere about how to implement tableViews.