UITableView: using moveRowAtIndexPath:toIndexPath: and reloadRowsAtIndexPaths:withRowAnimation: together appears broken - ios

I want to use iOS 5's nifty row-movement calls to animate a tableview to match some model state changes, instead of the older-style delete-and-insert.
Changes may include both reordering and in-place updates, and I want to animate both, so some rows will need reloadRowsAtIndexPaths.
But! UITableView appears to be just plain wrong in its handling of row reloads in the presence of moves, if the updated cell shifts position because of the moves. Using the older delete+insert calls, in a way that should be equivalent, works fine.
Here's some code; I apologize for the verbosity but it does compile and run. The meat is in the doMoves: method. Exposition below.
#define THISWORKS
#implementation ScrambledList // extends UITableViewController
{
NSMutableArray *model;
}
- (void)viewDidLoad
{
[super viewDidLoad];
model = [NSMutableArray arrayWithObjects:
#"zero",
#"one",
#"two",
#"three",
#"four",
nil];
[self.navigationItem setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:
#ifdef THISWORKS
#"\U0001F603"
#else
#"\U0001F4A9"
#endif
style:UIBarButtonItemStylePlain
target:self
action:#selector(doMoves:)]];
}
-(IBAction)doMoves:(id)sender
{
int fromrow = 4, torow = 0, changedrow = 2; // 2 = its "before" position, just like the docs say.
// some model changes happen...
[model replaceObjectAtIndex:changedrow
withObject:[[model objectAtIndex:changedrow] stringByAppendingString:#"\u2032"]];
id tmp = [model objectAtIndex:fromrow];
[model removeObjectAtIndex:fromrow];
[model insertObject:tmp atIndex:torow];
// then we tell the table view what they were
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:changedrow inSection:0]]
withRowAnimation:UITableViewRowAnimationRight]; // again, index for the "before" state; the tableview should figure out it really wants row 3 when the time comes
#ifdef THISWORKS
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:fromrow inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:torow inSection:0]]
withRowAnimation:UITableViewRowAnimationAutomatic];
#else // but this doesn't
[self.tableView moveRowAtIndexPath:[NSIndexPath indexPathForRow:fromrow inSection:0]
toIndexPath:[NSIndexPath indexPathForRow:torow inSection:0]];
#endif
[self.tableView endUpdates];
}
#pragma mark - Table view data source boilerplate, not very interesting
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return model.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#""];
if (cell == nil)
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#""];
[cell.textLabel setText:[[model objectAtIndex:indexPath.row] description]];
[cell.detailTextLabel setText:[NSString stringWithFormat:#"this cell was provided for row %d", indexPath.row]];
return cell;
}
What the code does: sets up a tiny model (small mutable array); when a button is pushed, it makes a small change to the middle element of the list, and moves the last element to be the first. Then it updates the table view to reflect these changes: reloads the middle row, removes the last row and inserts a new row zero.
This works. In fact, adding logging to cellForRowAtIndexPath shows that although I ask for row 2 to be reloaded, the tableview correctly asks for row 3 because of the insert once it's time to actually do the update. Huzzah!
Now comment out the top #ifdef to use the moveRowAtIndexPath call instead.
Now the tableview removes row 2, asks for a fresh row 2 (wrong!), and inserts it in the final row-2 position (also wrong!). Net result is that row 1 moved down two slots instead of one, and scrolling it offscreen to force a reload shows how it's gone out of sync with the model. I could understand if moveRowAtIndexPath changed the tableview's private model in a different order, requiring the use of the "new" instead of "old" index paths in reloads or model fetches, but that's not what's going on. Note that in the second "after" pic, the third and fourth rows are in the opposite order, which should't happen no matter which cell I'm reloading.
My vocabulary has grown colorful cursing Apple. Should I be cursing myself instead? Are row moves just plain incompatible with row reloads in the same updates block (as well as, I suspect, inserts and deletes)? Can anyone enlighten me before I go file the bug report?

I just spent some time playing with your code, and I agree; looks like it just doesn't work.
This whole area is a bit under-documented, but they don't actually say that you can mix moveRowAtIndexPath:toIndexPath: with reload methods. It does say in the that it can be mixed with row-insertion and row-deletion methods. Those seems to work if I modify your code to exercise those instead. So, you might be asking for an enhancement, not filing a bug. Either way, I'd definitely send it to radar.

Related

UITableView grouped section footer not updating after reload

I have 3 or 2 sections (depending on datasource), in my grouped UITableView. I am trying to reload the last section via:
dispatch_async(dispatch_get_main_queue(), ^{
[UIView performWithoutAnimation:^{
[feedDetailTB reloadSections:[NSIndexSet indexSetWithIndex:feedDetailTB.numberOfSections-1] withRowAnimation:UITableViewRowAnimationNone];
}];
});
First of all, the footer never disappears. The data source basically keeps track of whether there are more comments or not (a simple load more functionality). In the viewForFooterInSection I simply return nil, when all the comments have been loaded.
But, as you see in the GIF, at first the loading button stays there. It is even accessible and works. When I scroll up, it vanishes and one can see it in the bottom, which is correct. But after all the comments have been reloaded, it should vanish, but sadly it stays there.
If I use reloadData it works fine. But I can't used it, since I have other sections, which I don't need to reload.
Second, there is a weird animation/flickering of the row items, even when I have used UITableViewRowAnimationNone. Not visible in the GIF
You should implement "isTheLastSection" according to your logic
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
if (isTheLastSection) {
return 40;
}
return 0;
}
In order to add new rows to a section, you must use the insertRowsAtIndexPaths rather than just adding new objects to data source and reloading a section.
Here's the code:
NSMutableArray *newCommentsIndexPath = [[NSMutableArray alloc] init];
for (NSInteger i = currentCount; i < (_postDetailDatasource.commentsFeedInfo.allCommentsArray.count + serverComments.count); i ++)
{
NSIndexPath *idxPath = [NSIndexPath indexPathForRow:i inSection:sectionNumber];
[newCommentsIndexPath addObject:idxPath];
}
[_postDetailDatasource.commentsFeedInfo.allCommentsArray addObjectsFromArray:serverComments];
[feedDetailTB beginUpdates];
[feedDetailTB insertRowsAtIndexPaths:newCommentsIndexPath withRowAnimation:UITableViewRowAnimationFade];
[feedDetailTB endUpdates];

Objective-C - iOS UITableView bottom row crash on delete

I am extending the learn iOS programming today tutorial to include delete functionality.
I have modified the tableView didSelectRowAtIndexPath: method thusly:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
ToDoItem *tappedItem = [self.toDoItems objectAtIndex:indexPath.row];
if (tappedItem.completed) {
[tableView beginUpdates];
[self.toDoItems removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationBottom];
[tableView endUpdates];
} else {
tappedItem.completed = YES;
}
[tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}
In a section with three rows, it works as expected. Tapping produces a checkmark, tapping a check marked item deletes it. But if I tap the bottom row, it crashes with 'attempt to delete row 2 from section 0 which only contains 2 rows before the update'. Note this is happening when the other two rows are still there (my searches found numerous posts where there was a crash when someone was deleting the last remaining row--not the case here). The bottom row will mark itself completed just fine.
Also note, moving the array changing call out of the beginUpdates block changes the error from row 2 to row 3 ... contains 3 rows.
TIA for any assistance.
Edit:
I have fixed the problem by moving [tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone]; inside of the else block. Can someone explain why?
If you use deleteRowsAtIndexPaths, there is no point in trying to reload the row that you deleted. And, obviously, if you try to reload a cell for an indexPath that is no longer valid, then you will have the sort of problem you describe.
Let's say you have 10 rows, you don't want to say, effectively, "delete the tenth row; now reload the tenth row in a table that now only has nine rows." You can easily imagine why that is problematic.
In this case, you should remove the call to reloadRowsAtIndexPaths altogether. You only have to call reload... if the contents of some of the remaining cells change. If you're just inserting or deleting rows, then just do that, and no call to reloadRowsAtIndexPaths is needed.

UITableViewCell delete button not disappearing

I'm using a UISegmentedControl to switch a UITableView between two datasets (think favorites and recents). Tapping the segmented control reloads the tableview with the different data set.
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:anim];
When the user swipes to delete a row it works fine. HOWEVER when the user switches datasets via the segmented control, the DELETED CELL gets re-used without altering it's appearance (i.e. the red 'DELETE' button is still there and the row content is nowhere to be seen). This appears to be the opposite problem that most people are seeing which is the delete button not appearing.
This is the delete code:
- (UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleDelete;
}
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
if ([self.current isEqualTo:self.favorites])
{
Favorite *fav = self.favorites[indexPath.row];
NSMutableArray *mut = [self.favorites mutableCopy];
[mut removeObjectAtIndex:indexPath.row];
self.favorites = mut;
self.current = self.favorites;
[self.tableView deleteRowsAtIndexPaths:#[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
The tableview is set to single select, and self.tableView.editing == NO. I have also tried using [self.tableView reloadData] and deleting/inserting the difference in rows from one dataset to the next. Neither works.
The UITableViewCell I'm using supplies no backgroundView or selectedBackgroundView
[EDIT]
Segmented Control Value Changed:
- (IBAction)modeChanged:(id)sender
{
if (self.listMode.selectedSegmentIndex == 1)
{
self.current = self.favorites;
}
else
{
self.current = self.recents;
}
// Tryin this:
[self.tableView reloadData];
// Tried this:
// [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
}
// Only 1 Section per table
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
{
return [self.current count];
}
Oh for the love of...
I wasn't calling [super prepareForReuse]; in my UITableViewCell subclass.
UGH.
I ran into the same thing: to "delete" a custom UITableViewCell, I was removing it from the table and putting it onto another list, which the user could then display in a modal view when they have regrets and want to put it back. In iOS7 (but not iOS6), the cells so moved had the big ugly "DELETE" button still on them, despite calling setEditing:NO and so on. (And in addition, the rest of the cell content was not drawn at all, even though inspecting the cells in the debugger showed that all the subpanes were still there.)
Unlike Stephen above, I hadn't overridden prepareForReuse, so that wasn't the problem. But it was related: in my case, the cells weren't created with a reuse identifier:
self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
And per the docs, "If the cell object does not have an associated reuse identifier, this method is not called." But apparently, in iOS7 at least, it should be.
So the solution, in my case, was to explicitly call this [cell prepareForReuse] on each cell as I loaded it into the new table.

Removing items from a UITableView without using edit

I am trying to delete rows in an array without having to use the edit function that Apple provide (Something along the lines of -(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath). So the user selects some of the rows, presses a button not in the table view, then those rows fade away. Apple provide something of sorts here.
The code I am using calls an array of rows to be deleted, which is defined elsewhere, removes the array population objects at those particular rows, and is supposed to remove all of the rows using the fade stuff.
- (void) deleteTableRow {
NSIndexPath *current;
NSLog(#"to be deleted: %#", toBeDeleted); //toBeDeleted is the array with the NSIndexPath items
for(int i=0; i < [toBeDeleted count]; i++) {
current = [toBeDeleted objectAtIndex: i];
[tableData removeObjectAtIndex:current.row]; //Remove the necessary array stuff
}
tv = [UITableView alloc];
[tv beginUpdates];
[tv deleteRowsAtIndexPaths:toBeDeleted withRowAnimation:UITableViewRowAnimationFade];
[tv endUpdates];
// Do whatever data deletion you need to do...
}
tv is defined in my header file and is a referencing outlet of my UITableView.
So here are my main questions:
Because my UITableView is not a UITableViewController (it is part of the view instead), is this even possible?
If it is possible, why is this not working?

iOS: Add / Update UITableView Cell Row by Row

I have been struggling with this for a week and my head is about to explode.
Basically I use Prototype Cell, in CellWillAppear I did a little customizations like background color. Nothing fancy.
Due to this, my table view is always empty at start up (no cell) unless the array (data source) is filled with something. So what I did was in NumberOfRowsInSection:
return dataArray.count < 10? 10 : dataArray.count
I am doing this because I would like to see at least some empty cells when there is no data.
Meaning it will show on start up at least 10 empty cells.
To add data to the cell, I call the delegate method in my tableviewcontroller each and every time to add one single entity in the data array (am doing this, because I think it would be faster than waiting until the whole array is filled then call [self.tableView reloadData];) and then refresh it by using reloadRowsAtIndexPaths. But it crashed every single time when it reached to index 10 (error: ... before update number of data was 10, but after update is 11).
What I really want is:
1.) prepare some data
2.) send it to uitableview controller and add it to an array there, instead of waiting and then sending a whole array to table view and refresh at once.
3.) reload just one row after the update (instead of using reloadData -> since I have different color of cell, the whole reload thing cause my table view flash madly).
The one thing I am doing to cell customization is in willDisplayCell:
What I did there is to change the background color of the cell. Again, nothing fancy.
But since there is no data at start up, no cell is ever visible (ui tablew with no cell at displayed at all), unless I did this
return dataArray.count < 10? 10 : dataArray.count;
just so there are at least 10 empty cells showing (WHY do I have to do the above just to display some customized empty cells beats me...).
Using reloadData is to refresh no problem, but since I am updating the data source array in table view every time data is ready instead of saving all prepared data to this array and send it over to table view to update by using reloadData, I would like to update row by row.
I kind of feel that the error comes from the fact that, if I add one item in the array and then call reloadRowsAtIndexPath, it will say "Ok, you had one item before, but after update there is 2! Inconsistency.."
I have already tried using [tableView beginUpdate]; and [tableView endUpdate];
Nothing has worked so far.....
So to sum up: how can I have different colors of cells showing even when the data array is empty on start up (just like the default ui table view with cells displaying completely even with no data) and update just one of the cells once a piece of data is ready instead of updating the whole ui table view with reloadData?
Many thanks in advance, please advise. Regards.
"how can I have different colors of cells showing even when the data array is empty"
Don't have an empty array, have a mutable array where all the members are initially empty strings, and replace those with your real data when you get it.
"update just one of the cells once a piece of data is ready"
Update your array with the new data, and then use reloadRowsAtIndexPaths:withRowAnimation: to update the table. If you want to see the table update row by row (slow enough to see), then put your data in a temporary array first, then add it one element at a time using performSelector:withObject:afterDelay:, calling reloadRowsAtIndexPaths:withRowAnimation: after each addition.
It's a little hard to tell exactly what you want, but here is an example of what I mean. This table displays 20 empty rows, all with different colors, for 2 seconds, then it replaces the empty strings in displayData with the strings in theData one by one at a rate of 10 per second.
#interface TableController ()
#property (strong,nonatomic) NSArray *theData;
#property (strong,nonatomic) NSMutableArray *displayData;
#end
#implementation TableController
- (void)viewDidLoad {
[super viewDidLoad];
self.displayData = [#[#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#"",#""] mutableCopy];
self.theData = #[#"One",#"Two",#"Three",#"Four",#"Five",#"Six",#"Seven",#"Eight",#"Nine",#"ten",#"Black",#"Brown",#"Red",#"Orange",#"Yellow",#"Green",#"Blue",#"Violet",#"Gray",#"White"];
[self.tableView reloadData];
[self performSelector:#selector(addData) withObject:nil afterDelay:2];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.displayData.count;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
UIColor *cellTint = [UIColor colorWithHue:indexPath.row * .05 saturation:1.0 brightness:1.0 alpha:1.0];
cell.backgroundColor = cellTint;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.textLabel.text = self.displayData[indexPath.row];
return cell;
}
-(void)addData {
static int i = 0;
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
if (i < self.displayData.count) [self performSelector:#selector(addData) withObject:nil afterDelay:.1];
}
If you don't want any delay between row updates, and you want to make it work when displayArray has a different number of rows that theData, this version of addData should work:
-(void)addData {
static int i = 0;
if (i < self.displayData.count && i< self.theData.count) {
[self.displayData replaceObjectAtIndex:i withObject:self.theData[i]];
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}else if (i >= self.displayData.count && i< self.theData.count) {
[self.displayData addObject:self.theData[i]];
[self.tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:i inSection:0]] withRowAnimation:UITableViewRowAnimationNone];
i++;
[self addData];
}
}

Resources