Unnatural jerk with UITableView whe cell height is changed dynamically - iphone

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

Related

Where to use scrollToRowAtIndexPath in iphone?

I am using [self.messageList scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:NO]; in cellForRowAtIndexPath and it is working fine, when a row is added in table array then it auto scroll upside but the problem is that when I try to see the top rows then it automatically come again last rows. And I am not able to see my all rows. Please let me know where I use this or any other approach to do this.
cellForRowAtIndexPath method is not called for all cell that you have. It is only called for visible cell in your TableView. When You Scroll UITableView then it ask to UITableView that you want to use reusable cell or add to new one. generally we use reusable cell so, it is use 1st no of cell and add at end of cell . it is not create another new cell at the end. so, when you scroll your UITableView it not scroll to end of cell it indexPath is similar to indexPath of first no of cell. so you can not scroll your UITableView at to Down.
use following code may be helpful for you.... :)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *CellIdentifier = [NSString stringWithFormat:#"S%1dR%1d",indexPath.section,indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier];
[self.tblView scrollToRowAtIndexPath:indexPath atScrollPosition: UITableViewScrollPositionTop animated: YES];
// your code
}
// your code
return cell;
}
If you write this code in your cell for row at index path, then it will get called every time you try to scroll manually coz this message is called to draw your cell and you will b directed to the row you specified. Try checking your indexpath in a IF--Else block and scroll only when a certain action happens.
cellForRowAtIndexPath is a method for creating a cell to view .When you scroll this method is called when a cell is viewed.So each time the method called the [self.messageList scrollToRowAtIndexPath:indexPath method(which is called inside cellForRow) is also called and the table scrolls down to that row!!!

Change an image in a UITableViewCell without reloading cell

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];

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]

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).

How do you change the textLabel when UITableViewCell is selected?

I want to change the textLabel and detailTextLabel of a cell when it has been selected.
I've tried the following, but no change occurs:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MyAppDelegate *appDelegate = (MyPhoneAppDelegate*)[[UIApplication sharedApplication] delegate];
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
cell.detailTextLabel.text = #"xxxxx";
cell.textLabel.text = #"zzzzz";
[tableView reloadData];
}
I agree, reloading the table view will actually dump and reload/display all the cells using tableView:cellForRowAtIndexPath: and use the original data, not the updated #"xxxxx" and #"yyyyy" in your tableView:didSelectRowAtIndexPath: method.
In a little test project I was able to change the labels upon selection with:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView cellForRowAtIndexPath:indexPath];
cell.textLabel.text = #"it was tapped";
}
You should not be trying to reload the table while a cell is selected. Instead, try
[cell setNeedsLayout]
after you make the above changes to the labels.
Also, is there a reason you're making a reference to the app delegate in the method?
Try to reload the cell you selected (described by indexPath) :
[yourTableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
Create a New iPad Project (Split View) and Now go through the Classes->Files. The easiest way's been given there. The XCode's Generated Codes.
Sample Code Lines :-
cell.textLabel.text = [NSString stringWithFormat:#"Row %d", indexPath.row];
You can use them in cellForRowAtIndexPath ||&& didSelectRowAtIndexPath ..
Not sure what you're trying to do with the delegate but you should try calling the tableView already instantiated; i.e. call
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath: indexPath];
Maybe I'm not clear
What I'm saying is that you are instantiating a new empty table view
UITableViewCell *cell = [**tableView** cellForRowAtIndexPath: indexPath]; //cell has nothing it is new.
consider replacing to call the old
UITableViewCell *cell = [**self.tableView** cellForRowAtIndexPath: indexPath]; //now you have one that has a textField already in it
Did you try to refresh only the selected cell instead of reloading the whole table ?
[cell setNeedsDisplay];
instead of
[tableView reloadData];
This will have better performance and I'm not but I suppose that selection is lost if you refresh the whole table (this may be the reason why you don't see any change in the end)....