uitableview cell delete, custom animation - ios

i am trying to create a custom animation for the deletion of table rows by overriding the deleteRowsAtIndexPaths method. i created the custom insert animation successfully and tried to reverse the animation for the deletion part but i get some errors: Assertion failure in -[UITableView _endCellAnimationsWithContext:]
and
NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.(rows after update must be equal to rows before the update).
If i don't override the deleteRows method and use tableView's default delete method, everything works fine, but not with my own method.
Here is the overriden method written in my tableViewController class:
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths
withRowAnimation:(UITableViewRowAnimation)animation
{
[[super tableView] insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates]; // Populates UITableView with data
[self.tableView beginUpdates];
for (NSIndexPath *indexPath in indexPaths) {
__block UITableViewCell *cell = [[super tableView] cellForRowAtIndexPath:indexPath];
if (cell) { // If indexPath isn't visible we'll get nil here
// Custom animation
CGRect frame = cell.frame;
frame.origin.x = cell.frame.size.width;
cell.frame = frame;
frame.origin.x = 0;
void (^animationsBlock)() = ^{
cell.frame = frame;
};
if ([[UIView class] respondsToSelector:
#selector(animateWithDuration:delay:usingSpringWithDamping:
initialSpringVelocity:options:animations:completion:)]) {
[UIView animateWithDuration:0.9
delay:0
usingSpringWithDamping:0.5
initialSpringVelocity:0
options:0
animations:animationsBlock
completion:NULL];
} else {
[UIView animateWithDuration:0.3
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:animationsBlock
completion:NULL];
}
}
}
}
deleteRowsAtIndexPathsMethod:
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths
withRowAnimation:(UITableViewRowAnimation)animation
{
[[super tableView] deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates]; // Populates UITableView with data
[self.tableView beginUpdates];
for (NSIndexPath *indexPath in indexPaths) {
__block UITableViewCell *cell = [[super tableView] cellForRowAtIndexPath:indexPath];
if (cell) { // If indexPath isn't visible we'll get nil here
// Custom animation
CGRect frame = cell.frame;
frame.origin.x = 0;
cell.frame = frame;
frame.origin.x = cell.frame.size.width;
void (^animationsBlock)() = ^{
cell.frame = frame;
};
if ([[UIView class] respondsToSelector:
#selector(animateWithDuration:delay:options:animations:completion:)]) {
[UIView animateWithDuration:0.9
delay:0
options:UIViewAnimationOptionCurveEaseIn
animations:animationsBlock
completion:NULL];
} else {
[UIView animateWithDuration:0.9
delay:0
options:UIViewAnimationCurveLinear
animations:animationsBlock
completion:NULL];
}
}
}
}
Edit: this code is inside a method, that runs whenever i modify a cell's object. I modify an object and save it to core data, then i retrieve all the updated objects using [self populateData], then i want to run the animations: delete the old cell and insert the new updated cell somewhere at the bottom of the tableView
[coreDataStack saveContext];
[self populateData];
[self.tableView beginUpdates];
int oldIndex = indexPath.row;
int newIndex = newIndexPath.row;
if(oldIndex<newIndex){
[self deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];
[self insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationTop];
} else if (oldIndex>newIndex) {
[self deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationRight];
[self insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationBottom];
} else {
[self.tableView reloadRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationMiddle];
}
[self.tableView endUpdates];

Related

Insert UITableViewCell With Animation and Scroll to it

My requirement is simple , i just need to add a TableCell with rightRowAnimation and scroll to it. It happens as long as i am with within tableView height , as i cross the height of tableView to add cell further , it adds cell but displays no animation when scrolling to it.
Attached is a simple project with a UITableView for reference , Appreciate your help !!
Sample Project with a simple UItableView and Add Button
Code
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"MyCell1"];
BOOL myBool = indexPath.row%2 == 0;
if(myBool){
cell.backgroundColor = [UIColor redColor];
}else{
cell.backgroundColor = [UIColor whiteColor];
}
cell.textLabel.text = [self.dataArray objectAtIndex:indexPath.row];
return cell;
}
-(IBAction)addTapped:(id)sender {
[self.dataArray addObject:#"New Object"];
// [_tableView beginUpdates];
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:self.dataArray.count-1 inSection:0]] withRowAnimation:UITableViewRowAnimationRight];
// [_tableView endUpdates];
[_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.dataArray.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
Use bellow method for the animation.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
cell.alpha = 0;
cell.layer.transform = CATransform3DMakeScale(0.5, 0.5, 0.5);
[UIView animateWithDuration:0.7 animations:^{
cell.alpha = 1;
cell.layer.transform = CATransform3DScale(CATransform3DIdentity, 1, 1, 1);
}];
}
change what ever your requirements
you can try this to animate row put in table.
-(void)viewWillAppear:(BOOL)animated
{
dispatch_async(dispatch_get_main_queue(), ^{
[UIView transitionWithView:yourtablename
duration:5.1f
options:UIViewAnimationOptionTransitionFlipFromTop
animations:^(void) {
[yourtablename reloadData];
} completion:NULL];
});
}
also you can change animation style.

App crashes on call to 'tableView endUpdates'

I'm currently working on an implementation of an inline UIDate picker inside of a UITableViewCell.
I'm able to show and hide this picker cell when I select the cell directly above where that cell should be inserted, which is the behavior that I expect. However, the app crashes if I select any other cells in the table view:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit_Sim/UIKit-3318.16.14/UITableView.m:1582
After looking at the accepted answer to this SO question, I added an exception breakpoint, and I've found out that the app is crashing at the call to [tableView endUpdates]; in didSelectRowAtIndexPath:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
// Check to see if the "Only Alert From" row was selected. The cell with the picker should be below this one.
if (indexPath.section == TimeOfDaySection && indexPath.row == HourTimeZoneRow && self.timePickerIsShowing == NO){
[tableView beginUpdates];
[self showTimePicker];
[tableView endUpdates];
} else{
[tableView beginUpdates];
[self hideTimePicker];
[tableView endUpdates];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
That said, I'm not sure how to proceed. If I comment out the call to [tableView endUpdates];, the app won't crash when other cells are selected, BUT the cell with the picker view won't hide. Does anyone have any suggestions? Thank you!
EDIT: Below is my code for showTimePicker and hideTimePicker:
- (void)showTimePicker
{
self.timePickerIsShowing = YES;
self.timePicker.hidden = NO;
//Create the index path where we insert the cell with the picker
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:HourTimeZoneRow + 1 inSection:TimeOfDaySection];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
self.timePicker.alpha = 0.0f;
[UIView animateWithDuration:0.25 animations:^{
self.timePicker.alpha = 1.0f;
//This is the row where the picker cell should be inserted
[self.tableView reloadRowsAtIndexPaths:#[[NSIndexPath indexPathForRow:HourTimeZoneRow + 1 inSection:TimeOfDaySection]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView reloadData];
}];
}
- (void)hideTimePicker {
self.timePickerIsShowing = NO;
self.timePicker.hidden = YES;
//Create the index path where we delete the cell with the picker
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:HourTimeZoneRow + 1 inSection:TimeOfDaySection];
[self.tableView beginUpdates];
//Delete the picker row
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
[UIView animateWithDuration:0.25
animations:^{
self.timePicker.alpha = 0.0f;
}
completion:^(BOOL finished){
self.timePicker.hidden = YES;
}];
}
EDIT 2: After reading this SO thread, I believe the problem may be with my numberOfRowsInSection method:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
switch (section) {
case NotificationsSection:
return TotalPreferencesRows;
break;
case RedZoneSection:
return TotalRedZoneRows;
break;
case TimeOfDaySection:
if (self.timePickerIsShowing) {
return TotalTimeOfDayRows + 1;
}
// else if (self.timePickerIsShowing == NO){
// return TotalTimeOfDayRows;
// }
else{
return TotalTimeOfDayRows;
}
return TotalTimeOfDayRows;
break;
default:
return 0;
break;
}
}
Not sure if this is the source of the crash, but it's not recommended to call beginUpdates multiple times which you're doing in
[tableView beginUpdates];
[self showTimePicker];
[tableView endUpdates];
because showTimePicker calls [self.tableView beginUpdates];
The problem is in your didSelectRowAtIndexPath, you call the hide method even though it might not be showing. Make the else clause into an else if, like this:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
// Check to see if the "Only Alert From" row was selected. The cell with the picker should be below this one.
if (indexPath.section == TimeOfDaySection && indexPath.row == HourTimeZoneRow && self.timePickerIsShowing == NO){
[tableView beginUpdates];
[self showTimePicker];
[tableView endUpdates];
} else if (indexPath.section == TimeOfDaySection && indexPath.row == HourTimeZoneRow && self.timePickerIsShowing == YES){
[tableView beginUpdates];
[self hideTimePicker];
[tableView endUpdates];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}

Springboard-like animation for deleting UICollectionViewCells

I am trying to figure out a way to animate the deletion of a UICollectionViewCell similarly to springboard in iOS 7. The deleted cell should shrink into nothingness and the remaining cells should slide over to fill its place (the sliding is the important part). I have tried:
• Setting the frame of each cell to the frame of the previous cell, like so:
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]];
CGRect rect = [self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:i - 1 inSection:0]].frame;
[UIView animateWithDuration:0.2 animations:^{
cell.frame = rect;
}];
But this did not work, as the cells did not move at all.
• Both of the following:
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:nil];
and
[UIView animateWithDuration:0.2 animations:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
}];
But these make the cells crossfade to their required spots, not slide.
I also found this project, which uses the following:
for (int i = index+1; i<[items count]; i++) {
SEMenuItem *item = [items objectAtIndex:i];
[UIView animateWithDuration:0.2 animations:^{
if (i < index + remainingNumberOfItemsInPage) {
int intVal = item.frame.origin.x;
if (intVal %3== 0)
[item setFrame:CGRectMake(item.frame.origin.x+2*item.frame.size.width, item.frame.origin.y-item.frame.size.height-5, item.frame.size.width, item.frame.size.height)];
else
[item setFrame:CGRectMake(item.frame.origin.x-item.frame.size.width, item.frame.origin.y, item.frame.size.width, item.frame.size.height)];
}
[item updateTag:item.tag-1];
}];
}
However, it is not in a UICollectionView, so it does not help me.
Note: I am doing this on an iPad, just in case it matters to you.
Any ideas?
Subclass UICollectionViewFlowLayout. Implement:
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *attr = [self layoutAttributesForItemAtIndexPath:indexPath]; // might need to replace self with super
attr.transform = CGAffineTransformMakeScale(0, 0);
return attr;
}
In addition to duci9y's answer, (using attr.transform = CGAffineTransformMakeScale(0,0);), the following makes the tiles slide. I can't believe I didn't think of this before.
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:#[indexPath]];
[dataSource removeObjectAtIndex:indexPath.row];
} completion:nil];

Animate UITableViewCells to new position AFTER deletion animation is complete

I've got a UITableView that allows the user to delete rows in it by swiping them left (very much like the app Clear). My issue is that all the UITableViewCells below the one being deleted animate up to their new frames while the animation for the deleted cell is happening. I want it to happen after the animation for the deleted cell finishes.
My code:
//Starts updating the table
[CATransaction begin];
[CATransaction setAnimationDuration:JTTableViewRowAnimationDuration];
[CATransaction setCompletionBlock: ^{
[self.spenderTable reloadData];
}];
[tableView beginUpdates];
if (state == JTTableViewCellEditingStateLeft)
{
[self.list.spendrItems removeObjectAtIndex:indexPath.row];
//!!Animation happens here!!
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationLeft];
}
[tableView endUpdates];
[CATransaction commit];
[tableView performSelector:#selector(reloadVisibleRowsExceptIndexPath:) withObject:indexPath afterDelay:JTTableViewRowAnimationDuration * 2];
Im also using this great UITableView.
Any ideas on how to achieve this?
Got this figured out....for anyone else interested. You can go two ways with:
Approach #1 (Wrap in animation block):
[UIView animateWithDuration:0.25 animations:^{
[[(JTTransformableTableViewCell *)[self.spenderTable cellForRowAtIndexPath:indexPath] contentView] setX:-320];
}completion:^(BOOL done){
[self.list.spendrItems removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone];
}];
Remember to manipulate the datasource in the completion block to avoid an internal consistency exception.
Approach #2 (inherit from UITableView):
Override the animation like so:
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation{
for (NSIndexPath *indexPath in indexPaths)
{
UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath];
[cell setFrame:CGRectMake(320, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
[UIView beginAnimations:NULL context:nil];
[UIView setAnimationDuration:1];
[cell setFrame:CGRectMake(0, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
[UIView commitAnimations];
}
}

UITableView deselectRowAtIndexPath calling UITableviewCell setSelected?

I have custom cells that perform a short animation when selected and should animate back to their first state when deselected.
Here is me setSelected
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
if (selected && animated) {
NSLog(#"animate");
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.chevronImage.transform = CGAffineTransformMakeRotation(M_PI);
[self.chevronImage setCenter:CGPointMake(self.chevronImage.center.x, self.chevronImage.center.y - 1)];
}
completion:nil];
}
if (!selected && animated) {
NSLog(#"unanimate");
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.chevronImage.transform = CGAffineTransformMakeRotation(0);
[self.chevronImage setCenter:CGPointMake(self.chevronImage.center.x, self.chevronImage.center.y + 1)];
}
completion:nil];
}
[super setSelected:selected animated:animated];
}
And here is the code that calls it:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (![selectedIndex isEqual:indexPath]) {
NSLog(#"select %i", indexPath.row);
[tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone];
}
if (selectedIndex != nil) {
NSLog(#"deselect %i", selectedIndex.row);
[tableView deselectRowAtIndexPath:selectedIndex animated:YES];
}
if (controlRowIndex != nil && [indexPath isEqual:controlRowIndex]) {
return;
}
indexPath = [self modelIndexPathforIndexPath:indexPath];
NSIndexPath *indexPathToDelete = controlRowIndex;
if ([indexPath isEqual:selectedIndex]){
selectedIndex = nil;
controlRowIndex = nil;
} else {
selectedIndex = indexPath;
controlRowIndex = [NSIndexPath indexPathForRow:indexPath.row + 1
inSection:indexPath.section];
}
[self.tableView beginUpdates];
if (indexPathToDelete){
[self.tableView deleteRowsAtIndexPaths:#[indexPathToDelete]
withRowAnimation:UITableViewRowAnimationFade];
}
if (controlRowIndex){
[self.tableView insertRowsAtIndexPaths:#[controlRowIndex]
withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView endUpdates];
}
This works fine if I select a row and then tap on it again to deselect it. However, if I tap on row 0 and then tap on row 1 I get both rows selected, row 0 never gets deselected.
I can't figure what's different, I'm passing the exact same value each time, but I can only get it to animate if it's also currently selected. I've verified that it's sending the correct indexPath so I'm at a bit of a loss.
EDIT: Added the full method code
I would have expected that you set the variable selectedIndex in didSelectRowAtIndexPath:, but I cannot see where you are setting it. Thus, it looks like you are comparing to the wrong index path.
Conceptionally, I would first do the deselection.
BOOL tappedSelectedCell = [selectedIndex isEqual:indexPath];
if (selectedIndex != nil) {
// deselect selectedIndex
selectedIndex = nil;
}
if (selectedIndex == nil && !tappedSelectedCell) {
// select indexPath
selectedIndex = indexPath;
}
or simpler, but slightly longer
if (selectedIndex == nil) {
// select indexPath
selectedIndex = indexPath;
return;
}
if ([selectedIndex isEqual:indexPath]) {
// deselect indexPath
selectedIndex = nil;
}
else {
// deselect selectedIndex
// select indexPath
selectedIndex = indexPath;
}

Resources