Problem with configuring a UITableView as a inclusive selection list - iphone

I am programing a UITableView to behave as an inclusive selection list. My table displays correctly and allows for multiple cells to be selected with check boxes. My problem is that cells which have been selected (cells contain a check mark to the right) loose their selected status when scrolled out of view (cells check mark disappears). I want the selections made to cells in the table to be preserved even if cells are scrolled out of view. Does anyone have any idea what is causing this?
Here is my code inside of my TableViewController class:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [widgetTitles_glob objectAtIndex:row];
cell.detailTextLabel.text = #"";
cell.textLabel.textColor = [UIColor blackColor];
cell.textLabel.font = [UIFont boldSystemFontOfSize:15];
cell.accessoryType = UITableViewCellAccessoryNone;
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:YES];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if (cell.accessoryType == UITableViewCellAccessoryNone) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
// Reflect selection in data model
} else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) {
cell.accessoryType = UITableViewCellAccessoryNone;
// Reflect deselection in data model
}
}
Any help would be very much appreciated.

When you are using UITableView correctly, only as many UITableViewCell instances are allocated as are needed to fit on the screen. When you scroll down a table, and a cell disappears off the top of the screen, it is relocated to the bottom.
Your delegate method, tableView:cellForRowAtIndexPath: is responsible for setting up a cell, either creating a new one or reconfiguring a recycled one.
The proper thing to do is use an array to store your checked/unchecked values. When didSelectRowAtIndexPath: is called, you update the cell and your array. When tableView:cellForRowAtIndexPath: is called, you configure the cell based on the values in the array.
Based on your comments, you are already doing the right thing in didSelectRowAtIndexPath:; you just need to use those values when you set up the cell instance, because that cell could represent a row that has already been checked. Check the array and then set cell.accessoryType accordingly.

In cellForRowAtIndexPath: you were assigining the accessoryType as none, so whenever you scroll that delegate is called and set the accessory type as none. So you should change your code.
I have also faced this problem once; I came up with a solution as follows.
Store the indexPath.row values of selected indexPath in an array (this code should be in didSelectRowAtIndexPath delegate) if it is deselected remove from that array. In cellForRowAtIndexPath: method I have used a for loop and check if that indexPath.row is present then change it's accessory type to checkmark else none.

Thanks for your help. It actually turns out that the reason why the cells were getting reset to UITableViewCellAccessoryNone was becasue of the following line of code inside of cellForRowAtindexPath:
cell.accessoryType = UITableViewCellAccessoryNone;
Removing this has fixed the table.

Related

Setting Max IndexPath higher than 10?

I've ran into a problem when creating a checklist in my latest app.
When I call didSelectRowAtIndexPath, it changes an imageView in a CustomCell. So when I click a row in my table, it switches the CustomCell image to a checkmark. It works fine, however, when I scroll down, I notice that it also set some of the other rows in my checklist. I've got it figured out that if I touch row #1...it then updates 1, 11, 21, 31, 41, etc.
How do I get it to JUST change the image on row #1? Does IndexPath max out at 10 somehow?
Thanks!!
didSelectRowAtIndexPath Code:
{
CustomCell *cell = (CustomCell *) [resultsTable cellForRowAtIndexPath:indexPath];
cell.puckSelect.image = [UIImage imageNamed:#"puck_c.png"];
[cell setNeedsDisplay]
}
My list has thousands of items, is that effecting this?
This is because your cells are being reused as you scroll through the list. Don't store state in a cell (i.e. which cell is selected)!
Always read the state of a cell from a data structure (NSArray etc).
What I tend to do is this:
In didSelectRowAtIndexPath, make a change to the data structure (e.g. set 'isSelected' for row 23 to YES)
Then use reloadRowsAtIndexPaths to force the table to reload this row
In cellForRowAtIndexPath, read from the data structure to decide if this row has a tick.
You're seeing cell reuse at work. When you want to change state, you cannot just update the cell itself, because iOS will recycle the cell when it goes off-screen and will reuse it in another row. You must make a record somehow of which rows are checked, and when a cell is prepared for display in -tableView:cellForRowAtIndexPath, set the value of puckSelect.image appropriately.
You can either change you data source in didSelectRowAtIndexPath, or set the value of a property that you check in cellForRowAtIndexPath. Something like this:
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = [self.theData objectAtIndex:indexPath.row];
if (indexPath.row == self.checkedIndexPath.row) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}else{
cell.accessoryType = UITableViewCellAccessoryNone;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
self.checkedIndexPath = indexPath;
[tableView reloadData];
}

UITableView moves cell contents around when scrolling

I have seen other questions on here related to this, but I have their solutions in place, and things still move around when I scroll.
Basically, I have a table of data and I want the first and only the first row to have a UITableViewCellAccessoryDisclosureIndicator. The problem is, when I scroll around, the indicator duplicates, deletes, and moves to other cells. Here is my code..
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = (UITableViewCell *) [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
if(indexPath.row == 0) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
cell.textLabel.text = [tabledata objectAtIndex:indexPath.row];
return cell;
}
The problem is something with the indexPath.row variable.. its somehow returning 0 in other cells that aren't the very first (when it comes to displaying cells). HOWEVER... the data is always right from my array(which implies the value HAS to be correct) and in my
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
method, i only wish to change screens when row 0 is clicked.. and regardless of where I have scrolled to, no cells trigger except the very first. So it seems THAT check is always correct while the one inside cellForRowAtIndexPath is not...
Most likely, you are re-using cells that already have the accessoryType set. You need to explicitly set it to NOT show on cells where it shouldn't.
if(indexPath.row == 0){
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
} else {
cell.accessoryType = UITableViewCellAccessoryNone;
}

UITableCell Reusing itself

I made a UITableViewCell and I have 20 rows in that table in which 5 at a time on screen.
I have set accessory view checkd mark in didSelect delegate method on which row is being selected. My concern is let suppose first row is selected and its accessory type is checked, now if i scroll the table we see the sixth one is also checked. I know that the cell is reusing itself and not creating itself again.
The model should be able to handle which cell is checked and which is not. To simplify the problem, you can keep an array which will the NSIndexPaths that should be checked. If only one can be checked at the time, an ivar of the type NSIndexPath is more than enough.
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([[aTableView cellForRowAtIndexPath:indexPath] accessoryType] == UITableViewCellAccessoryCheckmark)
{
// Ok this one is selected, so we will remove it from the Reference Array.
}
else
{
// Ok this one doesn't has a checkMark
// First add the checkmark
[[aTableView cellForRowAtIndexPath:indexPath] setAccessoryType:UITableViewCellAccessoryCheckmark];
// Add the NSIndexPath to the Array of references
}
}
In the delegate method
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *cellId = [NSString stringWithFormat:#"cell%d",indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId ];
if (cell==nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
reuseIdentifier:cellId ] autorelease];
}
}
Set different cell ID to different cell row.

What is wrong with my UITableView cellForRowAtIndex for Single Selection?

Below is code for UITableView, But when i scroll its behaves weirdly (too annoying)... This problem is due to reuseIdentifier.... but dont know how to solve..
- (UITableViewCell *)tableView:(UITableView *)tableView1 cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView1 dequeueReusableCellWithIdentifier:CellIdentifier];
NSInteger imgTag = 1;
NSInteger lblTag = 2;
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(2, 2, 52, 52)];
// Image:[UIImage imageNamed:[self.glassType objectAtIndex:indexPath.row]]];
imgView.tag = imgTag;
[cell.contentView addSubview:imgView];
[imgView release];
UILabel *lblName = [[UILabel alloc] initWithFrame:CGRectMake(60, cell.frame.size.height/4, 200, 21)];
// lblName.text = [self.glassName objectAtIndex:indexPath.row];
lblName.tag = lblTag;
[cell addSubview:lblName];
[lblName release];
}
NSInteger imgIndex = 2;
NSInteger lblIndex = 3;
((UIImageView *)[cell viewWithTag:imgTag]).image = [[self.glassType objectAtIndex:indexPath.row] objectAtIndex:imgIndex];
((UILabel *)[cell viewWithTag:lblTag]).text = [[self.glassName objectAtIndex:indexPath.row] objectAtIndex:lblIndex];
return cell;
}
- (void)tableView:(UITableView *)tableView1 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView1 cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
How to make Cell for row at index so that it remains constant even when scrolled??? Also how to make single selection in UITableView??
The answer is that you should not add subviews to your table cells outside of the "if (cell == nil) { ..." clause or they get added over and over again to the same cell when it gets re-used.
See my answer to this question for a more detailed explanation, including code for how to fix it:
cellForRowAtIndexPath memory management
You also cannot store state in table cells because as soon as they scroll offscreen they are recycled and re-appear at a different index in your table. You need to set up an array of model objects to store state for your table cells (such as what their accessory type should be). A more detailed explanation can be found in my answer to this question:
Looping through UITableViewCells of a UITableView
If you fix how you are adding subviews to the cells, and store your "ticked" state in an array of model objects as well as setting the cell.accessoryType (so that it can be restored when the cell is dequeued), then your approach to row selection is otherwise correct.
So put this in your tableView:cellForRowAtIndexPath: method, just before the return cell;:
MyModelObject *object = [self.arrayOfModelObjects objectAtIndex:indexPath.row];
BOOL isChecked = object.checked;
cell.accessoryType = isChecked? UITableViewCellAccessoryCheckmark: UITableViewCellAccessoryNone;
And in your tableView: didSelectRowAtIndexPath: method, get rid of the current logic and replace it with:
- (void)tableView:(UITableView *)tableView1 didSelectRowAtIndexPath:(NSIndexPath *)indexPath
for (int i = 0; i < [self.arrayOfModelObjects count]; i++)
{
MyModelObject *object = [self.arrayOfModelObjects objectAtIndex:i];
object.checked = (i == indexPath.row); // only check the one we just tapped
}
//refresh table to update the accessory views for all rows
[tableView1 reloadData];
}
Obviously replace the arrayOfModelObjects with your own model implementation. You could just use an array of NSNumber objects containing bools if you don't want to create a custom class for this purpose.
The recycling queue is like a pool where previously created Cells are stored before to reuse them. For example when you scrolls up, at the moment the cell disappears above, it is stored in the queue and becomes available for the cell that will appear at the bottom. Ok ?
Actually the number of cells really created is exactly the max simultaneous cell you can display in your table (in most cases from 3 to 8). In other words your if (cell == nil) code is executed (more or less from 3 to 8 times) at the first reloadData to create the pool of cells your table needs.
Then all you make on a cell is kept as it and appears again when you dequeue it. It's now easy to understand that, in your code, you have to make all strictly row-dependant settings outside the if (cell == nil) block. The same way, do not add subViews outside the if (cell == nil) block, you can imagine the thousands of subview you will add each time you reset a dequeued cell !
Tip: if you need some custom cleanup before reusing a cell (like to set an image to blank), you can create a custom UITableviewCell class and implements the prepareForReuse method.
Is it clear ?
Always reload your tableView in viewWillAppear method instead of viewDidLoad.
(void) viewWillAppear:(BOOL)animated{
[self.tableView reloadData];
}
This avoids most of all unexpected and annoying problems. :)

My UITable is re-using cells when it shouldn't!

I have a UITableViewController which has two sections. The first section shows a single cell with centered text, saying Add a new Slide. The second section show the current slides.
When a user taps on the Add a new slide cell, a new UITableVeiwController is pushed onto the stack that shows an editor. If the user saves the new slide (by tapping save), the cell is added to the data source and the editor is popped from the stack.
I have two problems:
When the editor is popped, if a cell was deleted before Add a new slide was tapped, the old cell shows up instead of the new one. Popping the UITableViewController (by tapping the automatically generated back button) fixes this, but I'd like this to not happen at all. (Originally, popping the table did not update after popping the editor, so I added [self.tableView reloadData]; to the viewDidAppear method.)
After a certain number of slides, the last slide on the list becomes the Add a new slide cell. I know that the data is being entered properly because another part of the app, which uses the same data source, updates correctly. The table supports editing in the second section, and when you swap the order of the cells, it behaves correctly behind the scenes, but the wrong cell is still there.
What could be going on?
Here's some of my code:
Note that as I was gearing to post my code, I noticed a mismatch of the braces. The check for cell==nil seems to encompass the second part of the code which determines the content of the cells. This fixes the label of the cells in the second section of the table, but the style is still wrong. I've since fixed the code, but the original is posted here.
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
if ([indexPath section] == 0 ) {
cell = [[[MBTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}else if([indexPath section] == 1){
cell = [[[MBTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
if ([indexPath section] == 0) {
[cell.textLabel setTextAlignment:UITextAlignmentCenter];
[cell.textLabel setText:#"Add a New Slide"];
}else if([indexPath section] == 1){
NSArray *storedViewsInfo = [[NSArray alloc] initWithArray:[kSettings arrayForKey:#"views"]];
if ([[[storedViewsInfo objectAtIndex:[indexPath row]] valueForKey:#"type"] isEqualToString:#"announcement"]) {
[cell.detailTextLabel setText:#"Custom Announcement"];
[cell.textLabel setText:[[[storedViewsInfo objectAtIndex:[indexPath row]] valueForKey:#"info"] valueForKey:#"text"]];
}
[storedViewsInfo release];
[cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
}
}
return cell;
}
Without seeing the code, first thing that comes to mind is checking if you've given your custom cells different identifiers in your - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; method?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier1 = #"CellIdentifier1";
static NSString *Cellidentifier2 = #"CellIdentifier2";
if (indexPath.section == kAddSlideSection) {
CustomCell *cellType1 = (CustomCell*) [tableView dequeueReusableCellWithIdentifier:CellIdentifier1];
...
} else {
CustomCell *cellType2 = (CustomCell*)[tableView dequeueReusableCellWithIdentifier:CellIdentifier2];
...
}
}
Also it might be worth considering implementing a delegate method that gets called when your user finishes adding the new slide - i.e. if successful call [self.tableview reloadData] from that method instead of in viewWillAppear.