Objective-C - iOS UITableView bottom row crash on delete - ios

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.

Related

Programmatically select UITableView cell not in current view

So it's easy to select a row that is currently in the UITableView. For example, to select the first row:
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]
animated:YES
scrollPosition:UITableViewScrollPositionNone];
Say that I have an array that is the data source for the table and the array count is greater than the number of cells displayed in the tableview. How can I get the UITableView to scroll to an index from the array that is beyond what is currently being displayed in the tableview?
All I am trying to do is to replicate programmatically what a user would do with their index finger as they scroll down the table.
My specific table displays 9 rows. My array has 20+ items. As the UIViewController loads, it retrieves the row number that should be selected (from an integer stored in NSUserDefaults). But I find that it will only scroll to the correct array position if that integer value is between 0 and 8. If it is 9 or greater, nothing happens, and I can't figure out how to make it respond to this. I've looked at all the UITableViewDelegate methods and none seems to address this.
What I've been doing to scroll and select a specific row is this (example arbitrarily selecting row 11):
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:11 inSection:0]
atScrollPosition:UITableViewScrollPositionTop
animated:YES];
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:11 inSection:0]
animated:YES
scrollPosition:UITableViewScrollPositionTop];
Can anyone help me? I assume it isn't difficult, but I'm stuck.
Thanks!
Your cells that are off the screen ain't selecting because you are using reusable cells. The cells from the visible screen will be used later, it isn't that all 100 cells are cached and each cell is responsible for each row. What it means is that they could or couldn't have something in it already. For example, lets say you have cell for row 1. When it comes off the screen, in the next few cells it will be reused as cell 15 or something, and if it had selected properties, it will still have it. It is like a new job and you get a desk from the developer before you - you could have desk with his trash, but it could also be clean.
I wouldn't select them as you select them by method, but in if statement in your cellForRowAtIndexPath. Something along the lines (added comments):
- (UITableViewCell *) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = #"Cell";
// When using method with forIndexPath you don't have to check for nil because you will always get cell
MyTableCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
MyObj *obj = [self.myArray objectAtIndex:indexPath.row];
cell.location.text = obj.location.location_description;
// other formatting, text display, image loading, etc.
if ([self.selectedObjects containsObject:obj]) {
// do some selecting stuff
} else {
// but don't forget to unselect because you can get already selected cell
}
return cell;
}
Edit: To select invisible cell, first scroll to it, then select:
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
Try using UITableViewScrollPositionBottom instead of UITableViewScrollPositionNone
That is use this code
[self.tableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:10 inSection:0]
animated:YES
scrollPosition:UITableViewScrollPositionBottom];
I figured this out. My code was running in viewDidLoad which is too early. I needed to move it to viewDidAppear. At least I know that I am not losing my mind.

How to get last cell index path in iOS?

I am building an IOS application where I am using tableView. Now when I reached to last cell I load +10 data from localDB.
After fetching the data I reloaded the tableView that I don't need in place of reload I want to used updated tableView. for that SOF suggested me below code.
[[self tableView] beginUpdates];
[[self tableView] reloadRowsAtIndexPaths:____ withRowAnimation:UITableViewRowAnimationAutomatic];
[[self tableView] endUpdates];
I don't what should be there in reloadRowsAtIndexPaths value. Please can some help me in understanding this above line of code.
You can get the indexPath of the last row in last section like this.
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:(numberOfRowsInLastSection - 1) inSection:(numberOfSections - 1)];
Here, numberOfSections is the value you return from numberOfSectionsInTableView: method. And, numberOfRowsInLastSection is the value you return from numberOfRowsInSection: method for the last section in the table view.
You don't need to reload cells, because there are no cells after your 10th cell. You want your users to see more cells when they get to the last one as in a news app. The solution is to inset more rows below, here is how:
1- create the rows you want to insert
NSMutableArray *newRows = #[[NSIndexPath indexPathForRow:10 inSection:0]];
3- update your data source array
2- [self.tableView insertRowsAtIndexPaths: newRows, UITableViewRowAnimationAutomatic];

UITableView deletion of cell with animation? iOS (Objective C)

I've searched around, it seems that most recommendations are to simply [tableView reloadData].
However, I am after animation on deletion of the cell. If you can imagine a checklist, I add items to the checklist and once the item/task on the checklist (EG: "Get milk from milk bar") is completed I remove the item/task by tapping on it. At which point I want the cell to disappear with animation.
As mentioned above, most people just recommend a route which would involve me removing the item from the array it is held in and simply reloading the tableview, but that's completely void of animation.
Below is what I've tried as an alternative with little success.
[tableView beginUpdates]
[tableView deleteRowsAtIndexPaths:[self.taskArray objectAtIndex:indexPath.row] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates]
Note: The taskArray is populated with task's I enter. Additionally, the above code resides within the "didSelectRowAtIndexPath" method.
Any tips as to what im doing wrong or what I could do instead?
TLDR; I want to remove a cell with animation from the tableView simply by tapping on it.
Just a quick observation, the API is requesting that you provide an array of index paths and you only pass the actual item you're wanting for deletion.
What happens if you try:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
In your didSelectRowAtIndexPath
// Delete from the taskArray
[self.taskArray removeObjectAtIndex:indexPath.row];
// Remove row from the tableview
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];

Row deletion does not refresh table view in ios app

I have spent hours searching for the solution with out any luck. I am trying to delete a row (also deselect same row) programmatically. After row deletion call below, UITableViewDelgate methods get called expectedly and data source is updated but UITableView is not refreshed. deselectRowAtIndexPath call also does not work. I tried all kinds of scenarios as shown by commented lines.
Here is my code:
checkoutPerson is called as a result of observer listening for NSNotificationCenter messages.
- (void) checkoutPerson: (NSNumber*) personId {
Person *person = [_people objectForKey:personId];
if( person )
{
// Remove person from data source
int rowIndex = person.rowIndex;
S2Log(#"Deleting row number=%d", rowIndex);
[_allKeys removeObjectAtIndex:rowIndex];
[_people removeObjectForKey: personId];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
//[[self tableView] beginUpdates];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
S2Log(#"Deleting indexPath row=%d", [indexPath row]);
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
//[[self tableView] endUpdates];
S2Log(#"Reloading data");
//[[self tableView] reloadData];
//[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.5];
//[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:YES];
}
}
I will appreciate for help.
Thanks
-Virendra
I believe deleted cell is not being recycled. If I delete row in the middle, last row is always erased (since there is one less item) but the deleted row remains.
Use the above code between two function for table view
[tableView beginUpdates];
// the deletion code from data source and UITableView
[tableView endUpdates];
By calling this functions you are telling UITableView that you are about to make updates for deleting your cell.
Edit
The other problem I see with your code is you first delete the data from the data source.
Now you are asking for the UITableViewCell (which actually reloads the UITableView)
and then you are deleting the row from UITableView
I guess you should fetch the UITableViewCell before deleting values from your data source.
I found the problem. It has nothing to do with the code I posted above. It is syncing problem between visual display and the contents of data source. I have an embedded UITableView as part of a composite view. In composite view's controller, I was wiring up UITableView's delegate and data source to an instance of UITableViewController. Instead of this, I should have set UITableViewController's tableView property to the embedded UITableView. It seems that UITableView has to be contained within UITableViewController in order to correctly sync up table view visual display to the contents of data source. This also fixes row deselection and scrolling. I also needed to delay reloadData call in which case deleteRowsAtIndexPaths:withRowAnimation is not required. All you need is to modify the contents of your data source and call reloadData with a delay of 1.5 Seconds.
Thanks to all for great help.

When tapping an already selected cell in a UITableView, willSelectCellAtIndexPath gets called

I have a UITableView with two sections. Based on user interactions (selections and deselections), my datasource and UITableView are updated to move data between sections. Initially their is only data in section 0. When I tap a cell, willSelectCellAtIndexPath and didSelectCellAtIndexPath get called. As expected, when I tap the same cell again, didDeselectCellAtIndexPath is called.
Even after I begin to move data down to section 1 and select and deselect, the UITableView's delegate methods are called appropriately.
Once all data has been moved to Section 1, the UITableView begins to exhibit strange behavior. I can initially select a call and didSelectCellAtIndexPath is called. However, when I tap it again, didDeselectCellAtIndexPath is never called. Instead, any taps on the selected cell (I have confirmed it is indeed selected through [tableView indexPathsForSelectedCells] or any other cells in Section 1 only result in willSelectIndexPath and didSelectIndexPath getting called.
I have quite a bit of code in these delegate methods which is unrelated (I believe).... I do not explicitly change the selected state of a cell anywhere. I have posted willSelect method and can post more if necessary.
- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (remainingItemIsSelected && indexPath.section == 0) {
//other cells in the remaining items section are selected and a cell from that section is being selected
NSMutableIndexSet *arrayIndexesToBeDeleted = [[NSMutableIndexSet alloc] init];
for (NSIndexPath *previouslySelectedIndexPath in [tableView indexPathsForSelectedRows]) {
if (((ReceiptItem *)[self.remainingReceiptItems objectAtIndex:previouslySelectedIndexPath.row]).allocated == YES) {
//update data sources
NSLog(#"%#%i%#,%i",#"Section #:",previouslySelectedIndexPath.section,#" Row #:",previouslySelectedIndexPath.row);
[self.assignedReceiptItems addObject:[self.remainingReceiptItems objectAtIndex:previouslySelectedIndexPath.row]];
[arrayIndexesToBeDeleted addIndex:previouslySelectedIndexPath.row];
//update index path arrays
[self.receiptItemsToDeleteIndexPaths addObject:previouslySelectedIndexPath];
[self.receiptItemsToAddIndexPaths addObject:[NSIndexPath indexPathForRow:self.assignedReceiptItems.count-1 inSection:1]];
//update the pressed indexpath to equal to resulting indexpath to pass on to the didSelect method
if (previouslySelectedIndexPath.row < indexPath.row) {
indexPath = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:0];
}
}
}
//Delete assigned items from the remaining receipt items
[self.remainingReceiptItems removeObjectsAtIndexes:arrayIndexesToBeDeleted];
//update table (move allocated item down)
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:self.receiptItemsToDeleteIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:self.receiptItemsToAddIndexPaths withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView endUpdates];
if (self.remainingReceiptItems.count == 0) {
[tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationAutomatic];
}
//other cells in the remaining items section are selected and a cell from assigned items is being selected
return nil;
}
return indexPath;
}
From the Documentation for UITableViewDelegate:
tableView:willDeselectRowAtIndexPath:
This method is only called if there is an existing selection when the user tries to select a different row. The delegate is sent this method for the previously selected row.
If you think this through you will find that what you encounter is expected behavior. Tapping a row that is selected does not call will/didDeselctRowAtIndexPath on this row.
Instead, you could handle this in didSelectRowAtIndexPath for the selected row, i.e. deselect it there.
Possible Alternative
That being said, I think you are abusing the UITableView class. It is really not designed to do this moving stuff. You have no doubt noticed yourself that you have to write a lot of code to make this work -- the very reason you are encountering intractable errors.
It seems to me that a much cleaner (and ultimately more flexible) solution would be to have two separate table (or other) views that notify each other via delegates about datasource changes. Maybe a bit more work setting it up, but surely much less trouble down the road.

Resources