Delete relationship with UITableViewRowAnimationFade - ios

Say, I have an entity "Parent" which can have multiple "Child". During deletion, I do this:
[self.parent removeChildObject:self.childToRemove];
[self.managedObjectContext save:nil];
With only that, the scene does not change and the row is not removed. So what I did is I retrieve again all "Child" under that "Parent" and call
[self.tableview reloadData];
While it did it correctly, however, the deletion seems instantaneous. The row just disappear in a blink of an eye. I want it to animate with row fading.
I read that only the entity which was fetched using NSFetchedResultsController calls this delegate method "didChangeObject" where I put my row animation fade. However, for retrieving the "Child", I didn't fetched it using the controller, but I fetched it from
[self.parent children];

This bit of code should do it.
[self.tableView beginUpdates];
NSArray * rowIndexPathArray = #[childObjectIndexPath];
[self.tableView deleteRowsAtIndexPaths:rowIndexPathArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];

Related

Dynamically changing data source causing deleteRowsAtIndexPaths:indexes to crash

Tearing my hair out trying to get this to work. I want to perform [self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];,
More detailed code of how I delete:
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This works fine however if I get a push notification (i.e a new message received) whilst deleting the app will crash and display an error like:
Assertion failure in -[UITableView _endCellAnimationsWithContext:],
/SourceCache/UIKit/UIKit-3347.44/UITableView.m:1327
2015-07-04 19:12:48.623 myapp[319:24083] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason:
'attempt to delete row 1 from section 0 which only contains 1 rows
before the update'
I suspect this is because my data source is changing, the size of the array that
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
references while deleting won't be consistent because it was incremented by one when the push notification triggered a refresh. Is there any way I can work around this? Am I correct that deleteRowsAtIndexPaths uses the numberOfRowsInSection method?
So, in order to solve your problem you need to ensure that your data source will not change while some table view animations are in place. I would propose to do the following.
First, create two arrays: messagesToDelete and messagesToInsert. These will hold information about which messages you want to delete/insert.
Second, add a Boolean property updatingTable to your table view data source.
Third, add the following functions:
-(void)updateTableIfPossible {
if (!updatingTable) {
updatingTable = [self updateTableViewWithNewUpdates];
}
}
-(BOOL)updateTableViewWithNewUpdates {
if ((messagesToDelete.count == 0)&&(messagesToInsert.count==0)) {
return false;
}
NSMutableArray *indexPathsForMessagesThatNeedDelete = [[NSMutableArray alloc] init];
NSMutableArray *indexPathsForMessagesThatNeedInsert = [[NSMutableArray alloc] init];
// for deletion you need to use original messages to ensure
// that you get correct index paths if there are multiple rows to delete
NSMutableArray *oldMessages = [self.messages copy];
for (id message in messagesToDelete) {
int index = (int)[self.oldMessages indexOfObject:message];
[self.messages removeObject:message];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[indexPathsForMessagesThatNeedDelete addObject:indexPath];
}
for (id message in messagesToInsert) {
[self.messages insertObject:message atIndex:0];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[indexPathsForMessagesThatNeedInsert addObject:indexPath];
}
[messagesToDelete removeAllObjects];
[messagesToInsert removeAllObjects];
// at this point your messages array contains
// all messages which should be displayed at CURRENT time
// now do the following
[CATransaction begin];
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexPathsForMessagesThatNeedDelete withRowAnimation:UITableViewRowAnimationLeft];
[tableView insertRowsAtIndexPaths:indexPathsForMessagesThatNeedInsert withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
return true;
}
Lastly, you need to have the following code in all functions which want to add/delete rows.
To add message
[self.messagesToInsert addObject:message];
[self updateTableIfPossible];
To delete message
[self.messagesToDelete addObject:message];
[self updateTableIfPossible];
What this code does is ensures stability of your data source. Whenever there is a change you add the messages that need to be inserted/deleted into arrays (messagesToDelete and messagesToDelete). You then call a function updateTableIfPossible which will update the table view's data source (and will animate the change) provided that there is no current animation in progress. If there is an animation in progress it will do nothing at this stage.
However, because we have added a completion
[CATransaction setCompletionBlock:^{
updatingTable = NO;
[self updateTableIfPossible];
}];
at the end of the animations our data source will check if there are any new changes that need to be applied to the table view and, if so, it will update the animation.
This is a much safer way of updating your data source. Please let me know if it works for you.
Delete a Row
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
Delete a Section
Note : if your TableView have multiple section than you have to delete whole section when section contain only one row instead of deleting row
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexSet indexSetWithIndex:0];
[tableView beginUpdates];
[tableView deleteSections:#[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
I have a few test about your code above. And you cannot do that at the same time the least we can do is something like:
//This will wait for `deleteRowsAtIndexPaths:indexes` before the `setCompletionBlock `
//
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[tableView reloadData];
}];
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
[tableView endUpdates];
[CATransaction commit];
In your code whe have:
/*
int index = (int)[self.messages indexOfObject:self.messageToDelete];
[self.messages removeObject:self.messageToDelete];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
*/
This is not safe because if if the notification is triggered and for some reason [self.tableView reloadData]; called right before deleteRowsAtIndexPaths that will cause an crash, because the tableView is currently updating the datas then interrupt by the deleteRowsAtIndexPaths:, try this sequence to check:
/*
...
[self.tableView reloadData];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
This will cause a crash...
*/
Hmm.. Back to your code, Let's have a simulation that could cause the crash.. THIS is just an assumption though so this is not 100% sure (99%) only. :)
Let assume self.messageToDelete is equal to nil;
int index = (int)[self.messages indexOfObject: nil];
// since you casted this to (int) with would be 0, so `index = 0`
[self.messages removeObject: nil];
// self.messages will remove nil object which is non existing therefore
// self.messages is not updated/altered/changed
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
// indexPath.row == 0
NSArray *indexes = [[NSArray alloc] initWithObjects:indexPath, nil];
[self.tableView deleteRowsAtIndexPaths:indexes withRowAnimation:UITableViewRowAnimationLeft];
// then you attempted to delete index indexPath at row 0 in section 0
// but the datasource is not updated meaning this will crash..
//
// Same thing will happen if `self.messageToDelete` is not existing in your `self.messages `
My suggestion is check the self.messageToDelete first:
if ([self.messages containsObject:self.messageToDelete])
{
// your delete code...
}
Hope this is helpful, Cheers! ;)
'attempt to delete row 1 from section 0 which only contains 1 rows before the update'
This says your index path, during deletion, references row 1. However row 1 doesn't exist. Only row 0 exists (like the section, they are zero-based).
So how are you getting an indexPath one greater than the number of elements?
Can we see your notification handler that inserts a row? You could do some hack where if the animation is in process, you performSelector:withDelay: during the notification processing to allow time for the animation to complete.
In you code, the reason for crash is : you are updating array but not update data source of UITableView. so your dataSource also needs to reflect the changes by the time endUpdates is called.
So as per Apple Documentation.
ManageInsertDeleteRow
To animate a batch insertion, deletion, and reloading of rows and sections, call the corresponding methods within an animation block defined by successive calls to beginUpdates and endUpdates. If you don’t call the insertion, deletion, and reloading methods within this block, row and section indexes may be invalid. Calls to beginUpdates and endUpdates can be nested; all indexes are treated as if there were only the outer update block.
At the conclusion of a block—that is, after endUpdates returns—the table view queries its data source and delegate as usual for row and section data. Thus the collection objects backing the table view should be updated to reflect the new or removed rows or sections.
Example:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
Hope this help you.
I think the answer is alluded to in many of the comments here, but I'll try and tease it out a bit.
The first problem is that it doesn't look like you are bracketing your deletion method call in the beginUpdates/endUpdates methods. This is the first thing I would fix, otherwise as warned in Apple's documentation things can go wrong. (That doesn't mean they will go wrong, you're just safer doing it this way.)
Once you've done that, the important thing is that calling endUpdates checks the data source to make sure that any insertions or deletions are accounted for by calling numberOfRows. For example, if you delete a row (as you are doing), when you call endUpdates you best be sure that you've also deleted an item from the data source. It looks like you are doing that part right because you are removing an item from self.messages, which I assume is your data source.
(Incidentally you are correct that calling deleteRows... on its own without bracketing in beginUpdates/endUpdates also calls numberOfRows on the data source, which you can easily check by putting a break point in it. But again, don't do this, always use beginUpdates/endUpdates.)
Now it is possible that between calling deleteRows... and endUpdates someone else is modifying the self.messages array by adding an object to it, but I don't know if I really believe that because it would have to be extremely unfortunately timed. It's far more likely that when you receive a push message you aren't handling the insertion of the row properly, again by not using beginUpdates/endUpdates, or by doing something else wrong. It would be helpful if you could post the part of the code that handles the insertion.
If that part looks OK then it does look like you are very unfortunately making changes to the self.messages array on a different thread while the deletion code is being called. You can check this by adding a log line where your insertion code adds a message to the array:
NSLog(#"Running on %# thread", [NSThread currentThread]);
or just putting a break point in it and seeing which thread you end up on. If that is indeed the problem, you can dispatch the code that modifies the array and inserts the row onto the main thread (where it should be anyway since it's a UI operation) by doing something like below:
dispatch_async(dispatch_get_main_queue(), ^{
[self.messages addObject:newMessage];
[self.tableView beginUpdates];
[self.tableView insertRows...]
[self.tableView endUpdates];
});
That will ensure that the messages array won't get mutated by another thread while the main thread is busy deleting the row.

Assertion failure in -[UITableView _endCellAnimationsWithContext:

I'm an amateur at best, and stuck on this error! surely something simple...
- (void)addTapped:(id)sender {
TechToolboxDoc *newDoc = [[TechToolboxDoc alloc] initWithTitle:#"New Item" thumbImage:nil fullImage:nil];
[_itemArray addObject:newDoc];
//[self.tableView beginUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:_itemArray.count-1 inSection:0];
NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
NSLog(#"%#",indexPath);
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle];
[self performSegueWithIdentifier:#"MySegue" sender:self];
//[self.tableView endUpdates];
it is breaking on the line the says
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
You need to add [UITableView beginUpdates] and [UITableView endUpdates] around:
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:YES];
From the class reference:
Note the behavior of this method when it is called in an animation
block defined by the beginUpdates and endUpdates methods. UITableView
defers any insertions of rows or sections until after it has handled
the deletions of rows or sections. This happens regardless of ordering
of the insertion and deletion method calls. This is unlike inserting
or removing an item in a mutable array, where the operation can affect
the array index used for the successive insertion or removal
operation. For more on this subject, see Batch Insertion, Deletion,
and Reloading of Rows and Sections in Table View Programming Guide for
iOS.
I think you are inserting the row in your tableView but not updating the number of rows at section that's why you are getting error in this line. So along with inserting the row You should also increase the array count or whatever you are using to show the number of rows in table view.
In my case, I was using Parse and I deleted a few users (one of which was my iPhone simulator). I got this error whenever refreshing the tableView.
I just deleted the app from the Simulator and made a new account and it works flawlessly.
#droppy's answer helped give me the lightbulb moment of what was wrong!
Hope that helps someone.

How to achieve UITableView row animation similar to Mail

In Mail, if you manually refresh a mailbox via pull to refresh, if you don't have any new emails, the view slides back up to hide the spinner but the appearance of existing cells don't change - they don't animate at all. But if you did have new emails, those animate in from the top. If you had no emails listed before the refresh, all the cells animate in quite nicely. Also, if you refresh and an existing email had been deleted, its disappearance is animated but none of the other cells animate besides the entire table moving up to take the place of the removed cell. I would like to achieve this same behavior in my app.
Currently, when my data is fetched for the first time, it immediately appears in the table without any animation. If some data is deleted and the user manually refreshes, the entire table is instantly updated so there is no animation of the cell disappearing, it's just instantly replaced with the row underneath it. The same behavior occurs if cells were already displayed and refreshing results in new data being added to the table - it just instantly appears.
How can I implement animation for appearance and disappearance of cells when I reloadData? But not animate existing cells if they don't change.
My setup is to fetch the data, parse the JSON into a data structure (array or dictionary), then call reloadData, and then cellForRowAtIndexPath gets the data from the structure. When the user refreshes, it performs those same steps again.
My attempts so far haven't been successful. I tried instead of simply calling reloadData, I call [self.tableView reloadSections:[NSIndexSet indexsetWithIndex:0] withRowAnimation:UITableViewRowAnimationTop]; but this only animates the first section and it always animates it, even if none of the data has changed. My other attempt also always animates it:
[self.tableView beginUpdates];
[self.tableView deleteSections:[NSIndexSet indexsetWithIndex:0] withRowAnimation:YES];
[self.tableView insertSections:[NSIndexSet indexsetWithIndex:0] withRowAnimation:YES];
[self.tableView endUpdates];
When inserting the rows, keep the index paths being inserted in an array, then:
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:arrayOfInsertedIndexPaths withRowAnimation: UITableViewRowAnimationTop];
[self.tableView endUpdates];
I haven't confirmed directly, but I'm pretty sure this will behave as you wish when now insertions are made.
EDIT - Row-accurate animation is going to require row-accurate knowledge about the change in the model. How to get the index paths being inserted depends on how you're getting the data. In the toughest scenario, you're just getting a new collection and will need to work out for yourself what has changed. Your model objects probably need to implement equivalence** so you can conclude that different instances should be treated as the same.
** as in NSString isEqualToString: which is an equivalence test, not an equality test as it's name might suggest.
We're comparing the objects themselves, not row indexes, so knowing about deletions is hard, but no harder than knowing about insertions. As an example, here's a category I created for NSMutableArray that handles only insertions (it's ideal for an email or other timeline sort of app that's expecting new stuff, not deletions).
#import "NSMutableArray+Inserts.h"
#implementation NSMutableArray (NSMutableArray_Inserts)
// insert elements of array into the receiver, sorting on a key
// answer an array of index paths where the insertions happened
- (NSArray *)insertFromArray:(NSArray *)array sortedOn:(NSString *)key ascending:(BOOL)ascending {
// first insert everything from array (preserving uniqueness according to shallow equality)
for (id newElement in array) {
NSInteger position = [self indexOfObject:newElement];
if (position == NSNotFound) [self addObject:newElement];
}
// now sort using key
[self sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:key ascending:ascending]]];
// now find the insertions using deep equality
NSMutableArray *answer = [NSMutableArray array];
for (id newElement in array) {
NSInteger position = [self indexOfObject:newElement];
id element = [self objectAtIndex:position];
if (element == newElement) {
[answer addObject:[NSIndexPath indexPathForRow:position inSection:0]];
}
}
return [NSArray arrayWithArray:answer];
}
A couple of notes: 1) I lazily hardcoded the index path section to 0. If your model has multiple sections, you'll want to call this for each and add a section parameter so you get good index paths back, 2) the sorted insert feature of this is pertinent to my particular app, you might not need it, and can just remove those two params and delete the self sortUsing... line. 3) one way to account for deletions would be to add an analogous method that answers the index paths of elements in the receiver NOT in a passed array.

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.

Delete a row from Core Data UITableView with custom button

I have a UITableView that is populated with cells by core data and an NSFetchedResultsController. I have a custom button on my custom cells, which I'm planning on using to delete the cell. It's very easy to add the standard swipe-to-delete, but I'd rather use this custom button. Does anyone know how I could hook up an action to the button that would delete the entry from the data model and delete the cell from the UITableView? I cannot find a good solution to this for the life of me.
EDIT:
This is the code I have to delete it using standard swipe-to-delete. Any way I could modify it to work with a button?
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.tableView beginUpdates];
// Delete the task
Task *taskToDelete = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSLog(#"Deleting (%#)", taskToDelete.name);
[self.managedObjectContext deleteObject:taskToDelete];
[self.managedObjectContext save:nil];
// Delete the row
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self performFetch];
[self.tableView endUpdates];
}
General overview:
Connect the button to an action on your controller, deleteRow:(id)sender;
The sender will be the button. Get its superview, then the superview of its superview, and so on until you have a reference to the UITableViewCell. (Search in a loop using isKindOfClass:, don't assume the button is only 1, 2, 3 levels down.)
Call your table view's indexPathForCell: method to convert the cell reference to an index path.
Use objectAtIndexPath: on the fetched results controller to get the object.
Then delete it! If you are handling the NSFetchedResultsController delegate methods they will take care of removing the deleted row.
Sample code (typed without a compiler to check it):
- (void)deleteRow:(id)sender
{
id view = [sender superview];
while (view && ![view isKindOfClass:[UITableViewCell class]]) {
view = [view superview];
}
UITableViewCell *cell = view;
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
NSManagedObject *task = [fetchedResultsController objectAtIndexPath:indexPath];
[self.managedObjectContext deleteObject:task];
[self.managedObjectContext save:nil];
}
That's it. DO NOT try to remove the row from the table view. When the object is deleted, NSFetchedResultsController will detect the change and update your table view, assuming you set up the delegate methods as described in the documentation for NSFetchedResultsController. If you haven't, read the documentation (just option-double-click on "NSFetchedResultsController" in Xcode).

Resources