Change an image in a UITableViewCell without reloading cell - iphone

I'm writing an iPhone app with a UITableView as the primary user interface. Each section consists of two rows, a header and the body. When the user clicks on the header, I remove the second row by changing the numberOfRowsInSection value:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
cbwComponent *comp = [_componentController objectInListAtIndex:section];
if([comp hasContentsView] && !comp.contentsHidden){
return 2;
}else
return 1;
}
When the user selects the header, I'm using the following code:
comp.contentsHidden = YES;
[self.tableView beginUpdates];
NSArray *deleteIndexPaths = [[NSArray alloc] initWithObjects:[NSIndexPath indexPathForRow:1 inSection:indexPath.section], nil];
[self.tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
It's working great, with a nice smooth fade effect. The problem is, I'm trying to add an indicator in the header cell (row 0) that changes when it's clicked on. To change that image I have to refresh the top row as well as the second row, which makes the transition look bad (well, not nearly as smooth). Is there a way to change the image in a UITableViewCell without refreshing the cell?
Thanks
EDIT: I figured it out! You can maintain the smooth transition as long as you reload that first row before you make the change to the second row. It has to be called inside of [tableView beginUpdates];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[[NSArray alloc] initWithObjects:[NSIndexPath indexPathForRow:0 inSection:indexPath.section], nil] withRowAnimation:UITableViewRowAnimationNone];
...
[self.tableView endUpdates];
Did the trick.

You could also subclass a tableview cell and implement a view transition in it that can be called from your view controller. You could then call that without having to reload the cell.
[(YourCustomCell*)[tableView cellForRowAtIndexPath:indexPathOfYourCell] fadeInIndicator];

Related

Unnatural jerk with UITableView whe cell height is changed dynamically

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

Refreshing UITableview Section to One With Different Number of Rows

I've recently been working with a UITableView. It is dynamically populated once, then when a user selects an option, I want the list to change to a new one. I'm working with a grouped table with 3 sections and as you click on the rows the three groups need to be repopulated with a varying number of new rows. While my code works fine when there is the same number of rows in the new section as old, it crashes when that number changes. Interestingly though, it will wait to crash until it attempts to draw one of the cells that was there previously (the tableView still thinks the section has the old number of rows, tries to draw the cell that is no longer in my model object, and so I think it crashes because it's referencing a value in the new array that doesn't exist.
It crashes here:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell"];
if (cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"MyCell"];
}
if (indexPath.section==2){
//CRASH BELOW
cell.textLabel.text = [NSString stringWithFormat:#"%#", (NSString *)[[[[storyArray objectAtIndex:pageNumber]getChoices] objectAtIndex:(unsigned long)indexPath.row]objectAtIndex:0]] ;
}
return cell;
}
The function I use to reload the table is here:
-(void)changePage:(int)pageChangeNumber{
NSLog(#"The page change! Changing to: %#",[[storyArray objectAtIndex:pageChangeNumber]getTitle]);
pageHeader.text=[[storyArray objectAtIndex:pageChangeNumber] getTitle];
pageBody.text=[[storyArray objectAtIndex:pageChangeNumber] getBody];
[myTableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
[myTableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationFade];
[myTableView reloadSections:[NSIndexSet indexSetWithIndex:2] withRowAnimation:UITableViewRowAnimationFade];
[myTableView reloadData];
pageNumber=pageChangeNumber;
NSLog(#"Page Change Done");
}
I've also changed the numberofRowsInSection to be dynamic...
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
NSLog(#"Recounting Rows");
if (section==2){
return [[[storyArray objectAtIndex:pageNumber]getChoices] count];
} else {
return 0;
}
}
Any ideas on how I can get this working when the number of rows per section changes?
Thanks!
I don't know what crash you're getting, but I ran into crashes if the numberOfSectionsInTableView: or tableView:numberOfRowsInSection: methods returned different numbers of rows while the table was restructuring itself.
For example, UITableView calls those methods many times while it is redrawing (including during animations). In my backend, some of the values were changing during animation.
I had to take special care to synchronize those values before changing the UITableView
Before you update your table view's data and call reloadSections you need to first call [myTableView beginUpdates] and once you're done [myTableView endUpdates]

Smooth reload of uitableview data when rows are deleted

I am trying to reload a uitableview when it scrolls to the bottom of the screen. I delete the first few rows and add more rows to the bottom. Before the news rows are added to the uitableview's data source, i call the [tableview reloaddata] method. This is because i want the tableview to display the row which were previously visible on it.
It reloads the data correctly but there is a sudden jerk in the tableview. It flashes which doesn't give a nice user experience. So my question is
How to update the uitableview
when few rows from the top are
deleted without having the
jerking/flashing effect?
The current visible row should be retained in the refreshed view also.
Any ideas? There are no crashes as i update the data source correctly.
TIA,
Praveen S
delete the data from the data source in tableView delegate.
-(void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *key = [keys objectAtIndex:indexPath.section];
NSMutableArray *nameSection =[names objectForKey:key];
int itemID=item.ID;
if ([nameSection count]==1) {
[self.keys removeObjectsInArray:[NSArray arrayWithObject:key]];
[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
}
else {
[nameSection removeObjectAtIndex:indexPath.row];
[self.names setValue:nameSection forKey:key];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
}
}
check this too

How to create a toolbar between UITableView rows

I am interested on how tweetbot does the following:
I would like to create the same thing with my app, where if you click on a row, it pops
an additional UIToolBar and pressing on any other row will dismiss this view with animations.
The logic I think is simple, you just need to add a subView to the UITableViewCell when pressed and shift the rest of the content up, but how do you actually dismiss it when I press the other row?
The best way to do this is to add a dummy cell below the cell that was tapped.
First you need to keep track of what cell is been tapped and act accordingly.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//if user tapped the same row twice let's start getting rid of the control cell
if([indexPath isEqual:self.tappedIndexPath]){
[tableView deselectRowAtIndexPath:indexPath animated:NO];
}
//update the indexpath if needed... I explain this below
indexPath = [self modelIndexPathforIndexPath:indexPath];
//pointer to delete the control cell
NSIndexPath *indexPathToDelete = self.controlRowIndexPath;
//if in fact I tapped the same row twice lets clear our tapping trackers
if([indexPath isEqual:self.tappedIndexPath]){
self.tappedIndexPath = nil;
self.controlRowIndexPath = nil;
}
//otherwise let's update them appropriately
else{
self.tappedIndexPath = indexPath; //the row the user just tapped.
//Now I set the location of where I need to add the dummy cell
self.controlRowIndexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:indexPath.section];
}
//all logic is done, lets start updating the table
[tableView beginUpdates];
//lets delete the control cell, either the user tapped the same row twice or tapped another row
if(indexPathToDelete){
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPathToDelete]
withRowAnimation:UITableViewRowAnimationNone];
}
//lets add the new control cell in the right place
if(self.controlRowIndexPath){
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:self.controlRowIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
//and we are done...
[tableView endUpdates];
}
Whenever you have that dummy cell present you have to make sure to send the correct count.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(self.controlRowIndexPath){
return modelArray.count + 1;
}
return self.modelArray.count;
}
Also, return the appropriate height for your ControlCell.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if([indexPath isEqual:self.controlRowIndexPath]){
return 45; //height for control cell
}
return 70; //height for every other cell
}
Lastly, remember the control cell is a dummy. Is not part of the model, thus you have to account for that. If the user taps a row that is above the last tapped row is ok but when the new tapped row is below that control cell you have to make sure you access the right row in your model. In other words, account for that fake cell in the middle of your view.
- (NSIndexPath *)modelIndexPathforIndexPath:(NSIndexPath *)indexPath
{
int whereIsTheControlRow = self.controlRowIndexPath.row;
if(self.controlRowIndexPath != nil && indexPath.row > whereIsTheControlRow)
return [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0];
return indexPath;
}
I hope this helps.
In tableView:didSelectRowAtIndexPath:, you remove the tool view from the last selected cell. If there is no such view, you create a new one. Then you add this view to the newly selected cell. Save the indexPath of the selected row.
In tableView:heightForRowAtIndexPath:, you check if the indexPath is the same as the saved indexPath. If they are equal, you return a height that is the height of both views. If it is not equal, just return the height of the "real cell".
Put all your calls in didSelectRowAtIndexPath between [tableView beginUpdates] and [tableView endUpdates] to get animation for the height change.
rjgonzo's code works fine except for the case where you only have 1 row in the tableview. When there's only 1 row (and 1 object in the tableview datamodel) you'll get an NSRange exception when you call insertRowsatIndexPath(s). To fix this I checked to see if the datamodel has only 1 object and if so then I add the row to the current indexpath (instead of the controlindexpath) which results in the row logically being added above the first row and then I call moveRowAtIndexPath to interchange the rows after calling [self.tableView endUpdates]. The animation shows as expected with the control row appearing to slide down from the 1st row.
if(self.controlRowIndexPath){
//Check to see if the datamodel only has 1 object
if([self.objects count]==1){
//If so then insert the row above the row
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
else
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:self.controlRowIndexPath]
withRowAnimation:UITableViewRowAnimationNone];
}
[self.tableView endUpdates];
//if the row was inserted above the 1st row then switch the rows
if([self.objects count]==1)
[self.tableView moveRowAtIndexPath:self.controlRowIndexPath toIndexPath:indexPath];
I would not add a subview to a UITableViewCell, I would add another row to the UITableView. That way, the UITableView will take care of the animation. (And I don't think that's possible to animated UITableViewCell height changes...)
Use simply
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
do add a row. And
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
to remove it.
#rjgonzo this works great but there is a minor issue on how you keep the indexPathToDelete. Since it's just another pointer to self.controlRowIndexPath, once you clear or reassign the self.controlRowIndexPath, indexPathToDelete will not be what you wanted, and tableView deleteRowsAtIndexPaths:withRowAnimation: call, you will get an SIGBART crash.
so, instead of
//pointer to delete the control cell
NSIndexPath *indexPathToDelete = self.controlRowIndexPath;
and
//lets delete the control cell, either the user tapped the same row twice or tapped another row
if(indexPathToDelete){
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPathToDelete]
withRowAnimation:UITableViewRowAnimationNone];
}
the following code should work fine:
//pointer to delete the control cell
NSIndexPath *indexPathToDelete = [NSIndexPath indexPathForRow:self.control_row_index_path.row inSection:self.control_row_index_path.section];
...
...
//lets delete the control cell, either the user tapped the same row twice or tapped another row
if(indexPathToDelete.row != 0){
NSLog(#"row to delete %d", indexPathToDelete.row);
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPathToDelete] withRowAnimation:UITableViewRowAnimationNone];
}
Here's what I'm doing to get the animation to be clean.
In addition to the strategy user fluchtpunkt suggests (that is, adding a subview to the cell, updating the cell height via heightForRowAtIndexPath, beginUpdates, and endUpdates), I'm finding the following measures to be helpful with the animation:
The tableviewcells have a background image. Otherwise, the added subview/toolbar is visible through the cell just before the tableview animates the height change.
The tableviewcell is 'behind' the view that is the cell below it, otherwise again the subview/toolbar will show too soon. I'm using [tableview sendSubviewToBack:cell]; and it's taking care of that.
This is clean for me, but not exactly like Tweetbot. Interestingly, Tweetbot's animation seems to pull the toolbar down as the bottom of the cell animates down. It seems like some additional animation must be taking place, or my conspiracy theory is that it is actually adding the subview to the top of the cell below the selected cell, and then performing the lengthening and animation on the cell below.

UITextField in UITableViewCell - adding new cells

I am trying to create a table view similar to the YouTube video uploader view in the Photo Gallery on the iPhone.
Here's the basic setup.
I have a custom UITableViewCell created that contains a UITextField. Displaying the cell in my table works great and I can edit the text with no problems. I created an event hook so I can view when the text has changed in the text field.
[textField addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged]
What I want to do is this. When the user first edits the text I want to insert a new cell into the table view below the current cell (newIndexPath is calculated prior to the proper position):
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
Problem is when I run the cell insert code the cell is created but the text field's text updated briefly, but then the keyboard is dismissed and the text field is set back to an empty string.
Any help would be awesome! I've been banging my head about this one all day.
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section
{
if (section == 0)
return 2;
else
return self.tags.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
cell = (SimpleTextFieldTableCell *)[tableView dequeueReusableCellWithIdentifier:tagCellIdentifier];
if (cell == nil)
{
cell = [[[NSBundle mainBundle] loadNibNamed:#"SimpleTextFieldTableCell" owner:nil options:nil] lastObject];
}
((SimpleTextFieldTableCell *)cell).textField.delegate = self;
((SimpleTextFieldTableCell *)cell).textField.tag = indexPath.row;
((SimpleTextFieldTableCell *)cell).textField.text = [self.tags objectAtIndex:indexPath.row];
[((SimpleTextFieldTableCell *)cell).textField addTarget:self action:#selector(textFieldDidChange:) forControlEvents:UIControlEventEditingChanged];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
- (void)textFieldDidChange:(id)sender
{
UITextField *textField = sender;
[self.tags replaceObjectAtIndex:textField.tag withObject:textField.text];
if (textField.text.length == 1)
{
[textField setNeedsDisplay];
[self addTagsCell];
}
}
- (void)addTagsCell
{
NSString *newTag = #"";
[self.tags addObject:newTag];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:self.tags.count - 1 inSection:1];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
}
Only thing i can think of here is that perhaps when you insert a row the whole table view is reloaded, if you did not add your cell properly to the cell queue, they wont come back in the state that they were , therefore you are seeing empty cells as a result of the insert, just a guess, hope it helps.
Quick update:
Shouldn't matter but I notices you don't need:
[self.tableView beginUpdates]
[self.tableView endUpdates]
since you are performing one operation. Not sure if that matters (it shouldn't).
Update:
I should have said that such issues are pretty common. Here is a post related to your issue
http://www.bdunagan.com/2008/12/08/uitextview-in-a-uitableview-on-the-iphone/
Also, others have abstracted this out. Specifically I have tried this with no such issues:
http://furbo.org/2009/04/30/matt-gallagher-deserves-a-medal/
You could use:
http://github.com/joehewitt/three20/
But it has a bit of a learning curve.
Another Stackoverflow question tackling this issue:
Editing a UITextField inside a UITableViewCell fails
Excuse me not answering your issue directly, but I think that the solution might be contained in one of these links.
Original Answer:
Try:
[newTextField setNeedsDisplay];
Sometimes the tableviews can be "sticky" with updating UITextView/UITextField content.
If that doesn't work, be sure that you're backing model is also updated properly. You haven't displayed any code indicating you updated the model (although I assume you did, otherwise it would have likely thrown an exception).