Expanding table view cells disappearing - iphone

I have cells that expand by changing their height with a setExpanded: method call.
I then call reloadRowsAtIndexPaths: to refresh the cells.
The problem is the cells simply disappear and randomly re-appear. I suspect this has to due with the way the indexing is working.
If I call reloadData or beginUpdates/endUpdates the cells work as expected, but I lose the animations.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
JVCell *cell = (JVCell*)[tableView cellForRowAtIndexPath:indexPath];
JVCell *previousCell = nil;
if( previousIndexPath_ != nil ) // set to nil in viewDidLoad
{
previousCell = (JVCell*)[tableView cellForRowAtIndexPath:previousIndexPath_];
}
// expand or collapse cell if it's the same one as last time
if( previousCell != nil && [previousIndexPath_ compare:indexPath] == NSOrderedSame && [previousCell expandable] )
{
[previousCell setExpanded:![cell expanded]];
NSArray *indexPathArray = [NSArray arrayWithObject:previousIndexPath_];
[tableView reloadRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationAutomatic];
}
else
{
// collapse previous cell
if( previousCell != nil && [previousCell expandable] )
{
if( [previousCell expanded] ) [previousCell setExpanded:NO];
NSArray *indexPathArray = [NSArray arrayWithObject:previousIndexPath_];
[tableView reloadRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationAutomatic];
}
// expand new cell
if( [cell expandable] )
{
[cell setExpanded:YES];
NSArray *indexPathArray = [NSArray arrayWithObject:indexPath];
[tableView reloadRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
previousIndexPath_ = indexPath;
// works as expected, but I lose the animations
//[tableView reloadData];
// works as expected, but I lose the animations
//[tableView beginUpdates];
//[tableView endUpdates];
}
EDIT: updated to include cellForRowAtIndexPath and heightForRowAtIndexPath methods:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSUInteger section = [indexPath section];
NSUInteger row = [indexPath row];
JVCellSectionData *sectionData = [sections_ objectAtIndex:section]; // NSArray of SectionData objects
NSArray *cellArray = [sectionData cells]; // NSArray of cells
UITableViewCell *cell = [cellArray objectAtIndex:row];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger section = [indexPath section];
NSUInteger row = [indexPath row];
JVCellSectionData *sectionData = [sections_ objectAtIndex:section];
NSArray *cellArray = [sectionData cells];
JVCell *cell = [cellArray objectAtIndex:row];
return [cell cellHeight]; // changed when selected with setExpanded: method
}
Edit 2: I made a Quicktime video of what was happening and extracted screen shots.
What I'm attempting to do is expand a cell, not replace, insert or delete cells. Each cell has one or more subviews. The height of the cell changes depending on whether it's 'expanded' or not. The content view has the subviews added to it, and it's clipToBounds property is YES. When the cells expands or collapses the height value changes along with the frame of the cell (including background view and selected background view). I've logged all the frame values before, during and after expansion, and they are all consistent with their state and position.
Keep in mind that this works normally on iOS 4.3, as shown below:

I don't see any difference in behavior between iOS 5.0 and 4.3.2 in the simulator, or 5.0 on my phone—the cells disappear in the same way on each. (I'd test on 4.2.1 on my older iPod touch but Xcode 4 can't. Grr..) It looks like there's an easy workaround, though: If you do both reloadRowsAtIndexPaths:withRowAnimation: after expanding/collapsing your cells and then the reloadData call after that, it appears to preserve the animation.
Here's a small demo project for this. It's hard to say what the actual problem was—it's just some odd side effect of how UIKit is doing the cell animation. If you subclass [UITableViewCell setAlpha:] you can see that the table view is setting the cell's alpha to 0 and leaving it there. Weird.
Edit: Well that's weird. It wasn't working for a bit (was doing the old behavior, with cells disappearing), but now it's right again. Maybe I was running the wrong version. Anyway, let me know if this works for you.

I had the same issue.
I confirm that using no animation when reloading the row works fine.
But it turns out the issue is caused by returning a cached UITableViewCell in cellForRowAtIndexPath when actually a fresh one is needed.
The doc for reloadRowsAtIndexPaths:withRowAnimation: says:
"Reloading a row causes the table view to ask its data source for a new cell for that row."
Your cellForRowAtIndexPath method is returning locally cached cells.
Returning a brand new cell fixed the issue in my case and no workarounds were needed...

#Javy,
Thanks for your question. I was developing table with similar behaviour: there are text views (UITextView) in my table view cells (UITableViewCell) that are :
expanding if new line is required to display text without scrolling, or
collapsing if new line was removed
So I found the same problem. In iOS 5.x when I was starting to type text it suddenly was becoming invisible. But in iOS 4.x everything works fine.
I have found the solution and it works well for me.
Solution: Just try to replace your animation type UITableViewRowAnimationAutomatic with UITableViewRowAnimationNone when reloading the particular cell.
Some additional code: reload both cell in one moment:
NSMutableArray *indexes = [NSMutableArray arrayWithCapacity:2];
// collapse previous cell
if( previousCell != nil && [previousCell expandable] )
{
if( [previousCell expanded] ) [previousCell setExpanded:NO];
[indexes addObject:previousIndexPath_];
}
// expand new cell
if( [cell expandable] )
{
[cell setExpanded:YES];
[indexes addObject:indexPath];
}
[tableView reloadRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationNone];
Hope it will help you.

Seems to me like the cell isn't redrawn properly.
Did you try something like:
[cell.backgroundView setNeedsDisplay]

you could try reloading the table view when the animations have completed? at the moment if your animations are fired, and then you call [tableView reloadData] the animations won't be complete, so the frame etc may get out of sync
you could change your set expanded method to take a flag as well as an animation completion block, which could contain the reload code
[cell setExpanded:YES completion:^
//reload code
}];

In:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
you must return a new cell. If cell you don't need to create a new one, you only need:
[tableView beginUpdates];
[tableView endUpdates];
There is no need to call reloadRowsAtIndexPaths.

Okay...
before you
previousIndexPath_ = indexPath;
and after you do all the expanding / contracting of heights etc; you need to do something like
[cellArray replaceObjectAtIndex:previousIndexPath_ withObject:previousCell];
[cellArray replaceObjectAtIndex:indexPath withObject:cell];
which means your cellArray needs to be
NSMutableArray* cellArray;

Related

UITableViewCell background disappears?

The UITableViewController in my app pulls data from a json data source. I have also created a custom UITableViewCell background using CG. There is a very interesting bug that happens and I have no idea why. I will walk you through what happens and how I recreate it:
Tap to enter table view.
Without scrolling the table at all I immediately tap on an item in view.
After tapping on that item I press the back button to return to the table view.
If I then scroll down the first cell to appear from off screen will not have my custom back ground. It will just be the default for a cell. Then if I continue to scroll down every 10th cell will have the same issue.
This bug only occurs in this exact process. If I were to scroll the table view at all before tapping on an item it would not happen.
Here is the relevant code for the tableview controller:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Will remove all of the used codes from the table if setting is enabled
if (self.shouldHideCodes) {
NSMutableArray *tempArray = [self.jsonCodeData mutableCopy];
[tempArray removeObjectsInArray:[self.usedCodes usedCodes]];
self.jsonCodeData = tempArray;
}
return [self.jsonCodeData count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
if (self.jsonCodeData) {
cell = [tableView dequeueReusableCellWithIdentifier:#"code cell"];
if ([cell isKindOfClass:[CodeCellTVC class]]) {
CodeCellTVC *tvcCell = (CodeCellTVC *)cell;
if (![tvcCell.backgroundView isKindOfClass:[CustomCellBackground class]]) {
tvcCell.backgroundView = [[CustomCellBackground alloc] init];
}
NSDictionary *codeDict = [self.jsonCodeData objectAtIndex:indexPath.row];
// Retrieve code string from dictionary
NSString *codeText = [NSString stringWithFormat:#"%#", [codeDict objectForKey:#"code"]];
tvcCell.codeTableLabel.text = codeText;
}
}
return cell;
}
The thing that confuses me is how it reacts. That when the bug happens every 10th cell has the issue and not every one. I don't have anything outside of these method's that deal with the tableviewcell itself.
I understood your problem, you did a wrong at the time of initializing the cell,Every time your intializing the cell, so that every time memory will allocate for that cell, it will create memory issue.
Edit the code like bellow it will work for you.
cell = [tableView dequeueReusableCellWithIdentifier:#"code cell"];
if(cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"code cell"];
}

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

Can not get uitoolbar in expandable cell to hide

Hello I am currently using Simon Lee's wonderful tutorial to expand my cells. Everything is working fine.
My cells are 100px, and when expanded the cell becomes 144px high. In the added 44px I have placed a toolbar with bar buttons. I've gotten the buttons to work but there is one problem.
When the cell is expanded I can tap any of the 100px and the cell closes, however when the cell is closed , tapping on the lower 44px of the cell causes the bar buttons to fulfill their actions. I'm assuming that it's still enabled if when hidden from site.
I have disabled user interaction in storyboard but can not get it to turn on when cell is selected and vice versa! If anyone could point me in the right direction that would work!
Simon said something about doing the following, but I'm not quite sure on where to exactly implement it! I've tried it everywhere!
for(NewsCell *cell in [self.tableView visibleCells]) {
BOOL cellIsSelected = [selectedIndexes objectForKey:indexPath];
[cell.detailToolbar setUserInteractionEnabled:cellIsSelected];
}
And here's some of my code:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// If our cell is selected, return double height
if([self cellIsSelected:indexPath]) {
return 144.0;
}
// Cell isn't selected so return single height
return 100.0;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Deselect
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
self.tableView.clipsToBounds = YES;
// Store cell 'selected' state keyed on indexPath
NSNumber *selectedIndex = [NSNumber numberWithBool:isSelected];
[selectedIndexes setObject:selectedIndex forKey:indexPath];
// This is where magic happens...
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
The clipsToBounds for the tableView in the didSelectRowAtIndexPath looks a little strange to me
self.tableView.clipsToBounds = YES;
Did you set the clipoToBounds to YES for your cells?
I figured it out on my own. It turns out that all I had to do was set the user interaction in my cell to off. and switch it on for the selected cell.
for(NewsCell *cell in [self.tableView visibleCells]) {
BOOL cellIsSelected = isSelected;
[cell.detailToolbar setUserInteractionEnabled:cellIsSelected];
}

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.

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