I'm having a strange problem with UITableView. When the user taps the Edit button, the tableview (which is a grouped view with multiple sections) is supposed to show delete buttons for each row--except for the final row in each section, which has a green add button.
When a user taps the green button, a new row is inserted, but now the final row gets a delete button. Even stranger, that delete button ACTS like an add button. So it seems there's a drawing glitch, rather than a problem in assigning the correct style. (Extensive NSLogging shows that the last cell is getting the Insert editing style correctly.)
I've tried setting setNeedsDisplay on the cell and the tableView, I've tried reloading that section/row/the entire table, but the issue persists. Any ideas on how to get UITableView to explicitly redraw the editing controls?
I had the same problem. It seems like the cell's gets reused too much..
You could fix it by enforcing new cells for the inserted cells by using different identifiers:
if([tableView isEditing] && indexPath.row == [items count]){
static NSString *CellIdentifier = #"AddCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// "add-cell" setup here.. e.g"
[[cell textLabel] setText:#"Tab to add new item"];
return cell;
}else{
static NSString *CellIdentifier = #"NormalCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
// normal cell setup here..
return cell;
}
I'm pretty sure you can just implement this in your UITableView delegate to prevent it from editing the last row:
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return NO if you do not want the specified item to be editable.
if (indexPath.row != lastRow) // whatever your last row is
return YES;
}
return NO;
}
Related
Here is what I want in my app. Shown below are two screenshots of the iPhone app Store:
I basically need a "Read More" feature just like it is used in the app store (See the "Description" section in the two images above). I am assuming that each section here (Description, What's New, Information etc.) is a table view cell. And the text inside is a UILabel or UITextField.
Here is what I have tried so far to add this feature:
NSString *initialText = #"Something which is not a complete text and created just as an example to...";
NSString *finalText = #"Something which is not a complete text and created just as an example to illustrate my problem here with tableviews and cel heights. bla bla bla";
NSInteger isReadMoreTapped = 0;
My cellForRowAtIndexPath function:
// Other cell initialisations
if(isReadMoreTapped==0)
cell.label.text = initialText;
else
cell.label.text = finalText;
return cell;
My heightForRowAtIndexPath function:
// Other cell heights determined dynamically
if(isReadMoreTapped==0){
cell.label.text = initialText;
cellHeight = //Some cell height x which is determined dynamically based on the font, width etc. of the label text
}
else{
cell.label.text = finalText;
cellHeight = //Some height greater than x determined dynamically
}
return cellHeight;
Finally my IBAction readMoreTapped method which is called when the More button is tapped:
isReadMoreTapped = 1;
[self.tableView beginUpdates];
[self.tableView endUpdates];
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:2 inSection:0]; // I need to reload only the third row, so not reloading the entire table but only the required one
NSArray* rowsToReload = [NSArray arrayWithObjects:rowToReload, nil];
[self.tableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone];
After doing all this, I do get the required functionality. The new height of that particular cell is calculated and the new text loaded into it. But there is a very unnatural jerk on the TableView which results in a bad User experience. That is not the case with the app store More button though. There is no unnatural jerk in its case. The TableView remains at its place, only the changed cell has its size increased with the text appearing smoothly.
How can I achieve the smoothness as done in the iPhone app store More button?
Your problem might come from reloading the row. You want to try to configure the cell properties directly. I usually use a dedicated method to configure my cell content so I don't have to reload rows.
- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if(isReadMoreTapped==0)
cell.label.text = initialText;
else
cell.label.text = finalText;
// all other cell configuration goes here
}
this method is called from the cellForRowAtIndexPath method and it will configure the cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
and you would call this method directly to avoid reloading:
isReadMoreTapped = 1;
[self.tableView beginUpdates];
[self.tableView endUpdates];
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:2 inSection:0];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:rowToReload];
[self configureCell:cell forRowAtIndexPath:rowToReload];
Please try the following changes to your code, I think it will fix your problem.
no need to set cell.label.text in heightForRowAtIndexPath; Please remove them.
in the readMoreTapped, update table is enough:
isReadMoreTapped = 1;
[self.tableView beginUpdates];
[self.tableView endUpdates];
Either remove the calls to:
[self.tableView beginUpdates];
[self.tableView endUpdates];
Or change to ensure that your reloading code is between them. I would remove them as a single row reload is handled well with the method you use:
[self.tableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone];
You just need to specify a row animation like fade.
Okay, I finally solved the problem with the help of Matthias's answer (the accepted answer) and my own optimisations. One thing that definitely should be done is to create a dedicated function like configureCell: forRowAtIndexPath: to directly configure cell properties (see Mathias's answer). Everything remains the same with Matthias's answer except:
Before, I was calculating the heights of each cell everytime the heightForRowAtIndexPath function was called without caching(saving) them anywhere and hence when [self.tableView beginUpdates]; and [self.tableView endUpdates]; were called each cell height was calculated again. Instead, what you have to do is to save these cell heights in an array/dictionary so that whenever the More button is tapped, you calculate the height of only the cell that was changed. For the other cells, just query the array/dictionary and return the saved cell height. This should solve any problems with the tableView scroll after the cell height update. If anyone else still face a similar issue as this, please comment on this post. I would be happy to help
I use tableview with reusable cells. On each cell I have a textField with text, which I can modify. If text is empty, I delete that cell.
Lets say that we had 100 rows and we want to modify row number 1: we tap on it, give an empty string #"", scroll down to position number 50 and tap on this cell.
What now is going is that we detect tap gesture on another cell and I call method textFieldDidEndEditing: to see should I remove this cell from tableview. I use cellForRowAtIndexPath: to get the modified cell.
The problem is that there appear other cells with empty textField. I delete modified cell, but only one. I think that this is a problem with reusable cells.
Can anybody can help me with this problem?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *ImageIdentyfier = #"StandardCellWithImageIdentifier";
StandardCellWithImage *cellImage = (StandardCellWithImage *)[tableView dequeueReusableCellWithIdentifier:ImageIdentyfier];
if(cellImage == nil) {
cellImage = [[StandardCellWithImage alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ImageIdentyfier];
}
cellImage.nameLabel.delegate = self;
Item *item = [self.mutableFetchResults objectAtIndex:indexPath.row];
cellImage.nameLabel.text = item.itemText;
cellImage.infoLabel.text = item.itemInfo;
cellImage.checkbox.userInteractionEnabled = YES;
cellImage.nameLabel.userInteractionEnabled = NO;
if(item.itemIsChecked.boolValue == YES) {
cellImage.checkbox.tag = indexPath.row;
[cellImage.tapGesture addTarget:self action:#selector(didSelectedImageAtIndexPath:)];
cellImage.checkbox.image = [UIImage imageNamed:#"checkbox-checked.png"];
} else {
cellImage.checkbox.tag = indexPath.row;
[cellImage.tapGesture addTarget:self action:#selector(didSelectedImageAtIndexPath:)];
cellImage.checkbox.image = [UIImage imageNamed:#"open-checkbox.png"];
}
return cellImage;
}
When you scroll from row 1 to row 50, already existing cells are reused - including your cell with empty textField. That is why you see it several times and why your delete routine removed only one instead of all.
Sounds like your cell creation at cellForRowAtIndexPath method needs fixing to make sure empty textfield is not automatically copied to recycled cells. Without seeing any code, this exercise is left to you.
Looked at code, thanx. Could not see any "easy" fix, so proposing that you should avoid the problem. So instead of checking cell taps, maybe you should check list scrolling.
The problem you have exists only because cell, which was being edited, was recycled due user scrolling the list. Therefore remove the problem by a) don't let user to scroll while editing text or b) stop text edit when user starts scrolling.
when your textFieldDidEndEditing is invoked finish , you should check whether the text is "" if it is "" I think you should delete it from the dataSource and then reloadData
your - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath method should write like this :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
XXXXXXXCell *cell = [tableView dequeueReusableCellWithIdentifier: kIdentifier];
if (cell == nil) {
//Init cell, only init
}
//Setup the cells detail, such as the textField.text and so on
return cell;
}
I have a weird issue in my tableview. I want to select rows and only want to show the checkmark. I have a tableview with multiple sections. Selecting the row and displaying the checkmark is fine, but 10 rows down the tableview, that row will also get selected??? When selecting more than one row, again, 10 lines down, multiple selections appear.
I am using:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)newIndexPath {
[tableView deselectRowAtIndexPath:[tableView indexPathForSelectedRow] animated:NO];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:newIndexPath];
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
}
}
That's not another row being selected, that's the same check-marked cell being reused. In tableView:didSelectRowAtIndexPath: you are using the cell to store state. In tableView:cellForRowAtIndexPath: you are not addressing whether the row should have a check mark or not.
What you will likely want to do is have an ivar NSMutableSet and in didSelectRow.. either add or remove the indexPath from the set, depending on it's checked status. Then in cellForRow.. set the accessoryType property based on the indexPath's membership in the set.
Edit Sorry I didn't catch the comments where you save the state to in a data model. In that case all you have to do is add the check in cellForRow.... Since this is a common question I'll leave the original answer up.
In my app if user press on any cell in UITableView then accessoryType of cell will be set to check mark like following
-(void)Check:(UITableView *)tableView Mark:(NSIndexPath *)indexPath
{
[self.tableView cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryCheckmark;
buttonCount++;
[selectedCellArray addObject:indexPath];
}
and if user press the same cell then uncheck will happen as follows
-(void)UnCheck:(UITableView *)tableView Mark:(NSIndexPath *)indexPath
{
[self.tableView cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryNone;
buttonCount--;
if (buttonCount == 0) {
[selectedCellArray removeAllObjects];
}
}
and i am calling this
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if([tableView cellForRowAtIndexPath:indexPath].accessoryType == UITableViewCellAccessoryCheckmark)
{
[self UnCheck:tableView Mark:indexPath];
}
else
{
[self Check:tableView Mark:indexPath];
}
Problem is when i am pressing on 1st cell it call Check method and mark the cell to but when i am scroll down i find 2-3 more cheked cell ...even i did not select those cell...i dont know why and how it checked automatically ...
i hope some one know where is the problem
thank you very much
Because cells will be reused by the tableview. Also set the checkmark/accessory type in your tableView:cellForRowAtIndexPath: method.
ya.. I see this happen a lot.
the correct pattern to manage the UITableViewCell state is NOT to directly manipulate the TableViewCell to update the UI, and always set, draw and create the correct state from cellForRowAtIndexPath (or tableView:willDisplayCell:forRowAtIndexPath:)
Meaning, if the user clicks on the cell and you need to update the UI of that cell, store the new state of that cell in an array or dictionary (I find that an NSMutableDictionary with the NSIndexPath as the key works very well).
then call the reloadRowsAtIndexPaths:withRowAnimation: or just [tableView reloadData] so that the cellForRowAtIndexPath reads that array or Dictionary and correctly draws the cell.
otherwise, the cellForRowAtIndexPath will constantly overwrite your changes and use recycled cells that hold incorrect state.
one exception to this rule would be if you would like a nice animation between the two states... if that is the case, save off your new state, perform your animation right there on the cell, then when the animation completes, call the same reloadRowsAtIndexPaths:withRowAnimation or reloadData so that the cell is redrawn in its new state.
I have a transparent table view (UIViewController with subview UIImageView, and another subview UITableView on top of the UIImageView sibling with background = clearColor, UITableViewCells background = clearColor). I also want taps on the cells to toggle the cell's accessoryType between checkmark and none. If I modify the UITableViewCell's accessoryType in tableView:didSelectRowAtIndexPath:, sometimes (30-50% of the time, both on the ios4.1 simulator and on a 3GS iphone running os4.1) when toggling from accessoryType None to accessoryType checkmark the checkmark image is painted against an opaque white background instead of a transparent background. If instead I reload the table (where the accessoryType is also set appropriately for each cell) the transparency works correctly 100% of the time.
Is this a bug? Or is modifying a cell in tableView:didSelectRowAtIndexPath: not the right thing to do, and that row should be reloaded instead? Or is there something else I'm missing?
edit: Here's my didSelectRowAtIndexPath code that shows the undesirable behavior:
edit 2: One more detail of what's happening. It's the very tail end of the deselect animation where the problem happens. The checkmark appears and is displayed properly & transparently while the deselect animation is running and the blue selection is gradually fading out. After the deselect animation finishes and the blue is all gone, maybe 1/10 of a second after the selection color is completely gone, is when the checkmark accessory "automagically" turns opaque with no further user input.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ( self.editing )
{
}
else
{
[self.myTableView deselectRowAtIndexPath:indexPath animated:YES];
UITableViewCell *cell = [self.myTableView cellForRowAtIndexPath:indexPath];
// toggle the selection status of the selected row
//
NSNumber *rowObj = [NSNumber numberWithUnsignedInt:indexPath.row];
if ( [self.selectedRows containsObject:rowObj] )
{
// currently selected, now de-select
//
cell.accessoryType = UITableViewCellAccessoryNone;
[self.selectedRows removeObject:rowObj];
}
else
{
// currently unselected, now select
//
cell.accessoryType = UITableViewCellAccessoryCheckmark;
[self.selectedRows addObject:rowObj];
}
}
}
I think it's cleaner if you move the toogle selection piece of code to
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
and on didSelectRowAtIndexPath call to
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
This way you will reload only the selected row as soon as it is selected, and no need for calling reloadData.
Hope this helps!
This post hasn't been updated in 4 months but I figured I would submit my solution as it seems to work and could help somebody else with a similar problem.
Everything happens in the didSelectRowAtIndexPath method. The self.view I call is a tableView (so replace it with your tableView's name, e.g. self.tableView)
// let's go through all the cells to update them as I only want one cell at a time to have the checkmark next to it
for (int i=0; i<[self.detailsArray count]; i++) {
// whenever we reach the cell we have selected, let's change its accessoryType and its colour
if (i == indexPath.row) {
// that's the row we have selected, let's update its
UITableViewCell *cell = [self.view cellForRowAtIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryCheckmark;
cell.textLabel.textColor = [UIColor colorWithRed:51.0f/255.0f green:102.0f/255.0f blue:153.0f/255.0f alpha:1.0f];
}
// if it isn't the cell we have selected, let's change it back to boring dark colour and no accessoryType
else {
UITableViewCell *cell = [self.view cellForRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
cell.accessoryType = UITableViewCellAccessoryNone;
cell.textLabel.textColor = [UIColor darkTextColor];
}
}
// don't forget to deselect the row
[self.view deselectRowAtIndexPath:indexPath animated:YES];
this UITableViewDelegate method:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
is called right before displaying a cell. Modifying your accessory view there should work.
In debugging another problem in an unrelated project with a custom table row selection animation, I found that if the cell selection style is not UITableViewCellSelectionStyleNone, then any changes to the cell performed in didSelectRowAtIndexPath are subject to glitches and other desirable results (the changes made to cell N do not show up until cell != N is selected later, and animations performed on the cell never show up at all). When I changed the selection style to UITableViewCellSelectionStyleNone, then all visual cell changes made in didSelectRowAtIndexPath show up right away without problems.