30

I've got a UITableView that is being populated by an NSFetchedResultsController.

On the initial load of the table I would like the Cells to be animated in but I would like to do a little bit more custom animation than

[tableView insertRowsAtIndexPaths:withRowAnimation:];

Allows. Specifically I would like to slow down the animations and have them fly in from the side in a specific way. I have not been able to achieve this by UITableViewRowAnimation constants. Is there a way to use Core Animation to do exactly what I want or do I need to stick with UIKit animations in this specific instance?

Thanks for any help!

Joel

Joel Bell
  • 2,718
  • 3
  • 26
  • 32

4 Answers4

38

NEWEST SOLUTION (2017-12-12)

Adding a Swift 4.0 version of the animate method. It should then be implemented in the same way as the solution below:

func animate() {
    for cell in self.tableView.visibleCells {
        cell.frame = CGRect(x: self.tableView.frame.size.width, y: cell.frame.origin.y, width: cell.frame.size.width, height: cell.frame.size.height)
        UIView.animate(withDuration: 1.0) {
            cell.frame = CGRect(x: 0, y: cell.frame.origin.y, width: cell.frame.size.width, height: cell.frame.size.height)
        }
    }
}

NEWER SOLUTION (2015-09-05)

Adding a Swift 2.0 version of the animate method. It should then be implemented in the same way as the solution below:

func animate() {
    for cell in self.tableView.visibleCells {
        cell.frame = CGRectMake(320, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)
        UIView.animateWithDuration(1.0) {
            cell.frame = CGRectMake(0, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)
        }
    }
}

NEW SOLUTION (2014-09-28)

I reworked the solution a bit to make the implementation more easier and to make it work with iOS8. All you need to do is to add this animate method in your TableViewController and call it whenever you want it to animate (for instance, in your reload method, but you could call it at any time):

- (void)animate
{
    [[self.tableView visibleCells] enumerateObjectsUsingBlock:^(UITableViewCell *cell, NSUInteger idx, BOOL *stop) {
        [cell setFrame:CGRectMake(320, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
        [UIView animateWithDuration:1 animations:^{
            [cell setFrame:CGRectMake(0, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
        }];
    }];
}

Again, change the animation how you like. This particular code will animate the cells in from the right at a slow rate.

OLD SOLUTION (2013-06-06)

You can do this by implementing your own UITableView and overriding the insertRowsAtIndexPaths method. Here is an example of how that could look like where the cells will be pushed from the right, really slowly (1 second animation):

- (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];
    }
}

You can play around with the animations yourself. This method will not be called automatically by the table view so you have to override the reloadData method in your table view delegate and call this method yourself.

COMMENT

The reloadData method should look something like this:

- (void)reloadData
{
    [super reloadData];
    NSMutableArray *indexPaths = [[NSMutableArray alloc] init];
    for (int i = 0; i < [_data count]; i++)
        [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
    [self insertRowsAtIndexPaths:indexPaths withRowAnimation:0];
}
Sunil Targe
  • 7,251
  • 5
  • 49
  • 80
Mattias Farnemyhr
  • 4,148
  • 3
  • 28
  • 49
  • Is it just about overriding `insertRowsAtIndexPaths:withRowAnimation:` method? When I override it and then call with `beginUpdates:` and `endUpdates:` method, the app crashes on `[UITableView _endCellAnimationsWithContext:]` – Martin Pilch Jun 05 '13 at 20:46
  • hi. if I try to subclass UITableView and override insertRowAtIndexPaths... My app will crash with follow error "rows contained in an existing section after the update (30) must be equal to the number of rows contained in that section before the update (29), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'" ---- It appears to me that after subclassing the row is not REALLY INSERTED anymore therefore is no longer consistent. Can you see why? – mkto Jun 20 '13 at 13:01
  • did you do the same reloadData method as I did (see my edit), the problem is that you can't insert another object into _data and call insertRowAtIndexPaths, you need to do it and do super reloadData to make sure the class knows it as new data before you do the animations. – Mattias Farnemyhr Jun 20 '13 at 15:42
  • Hi materik. Ok now I see it. Strictly speaking, insertRow with animation is commonly used without calling reloadData so this is kind of like defeat the purpose of insertRow. So Your method doesn't have to be overriding insertRow, it can be a new method itself. – mkto Jun 22 '13 at 04:19
  • sure, you're absolutely right, the example above doesn't need to override insertRow. – Mattias Farnemyhr Jun 23 '13 at 15:42
  • I followed your tutorial, and got some error in reloadData section 'use of undelcared identifier of data'. can you help me ? – novalagung Aug 29 '13 at 10:10
  • you need to declare _data outside the reloadData method, _data should be an array containing the data you want to display on each cell in the tableview. – Mattias Farnemyhr Aug 29 '13 at 11:15
  • `[self cellForRowAtIndexPath:indexPath];` returns nil with iOS 7.1 SDK. – derpoliuk Apr 04 '14 at 10:36
  • Did you consider calling super in your implementation? :) – pronebird May 03 '14 at 13:32
  • doesn't work for iOS8, uitableview calls reloaddata as soon as the table is initialized and that built in animation overwrites the one we've override. – mskw Sep 26 '14 at 18:40
  • @mskw try the new solution – Mattias Farnemyhr Sep 28 '14 at 17:59
  • The one I posted?haha – mskw Sep 28 '14 at 18:03
  • @mskw oh, sorry, missed that :) – Mattias Farnemyhr Sep 28 '14 at 18:22
  • @DavidRobertson sure. I will work on that right now. – Mattias Farnemyhr Sep 05 '15 at 15:12
  • @materik thank you for the effort! downloading Xcode 7 to try it out) unfortunately, simply changing the enumerate's syntax for earlier swift versions didn't work out. – David Robertson Sep 05 '15 at 16:17
9

materik's answer haven't worked for me with iOS 7.1 SDK, I've got nil after calling [self cellForRowAtIndexPath:indexPath];

Here's my code in subclass of UITableView:

- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths
              withRowAnimation:(UITableViewRowAnimation)animation
{
    [super insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
    [self endUpdates]; // Populates UITableView with data
    [self beginUpdates];
    for (NSIndexPath *indexPath in indexPaths) {
        __block UITableViewCell *cell = [super 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.3
                                      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];
            }
        }
    }
}
Community
  • 1
  • 1
derpoliuk
  • 1,756
  • 2
  • 27
  • 43
  • i am inserting row at top but i am getting cell nil on [super cellForRowAtIndexPath:indexPath] .is it because it is not visible yet if yes then how can i do animation for insertion – harpreetSingh Jun 08 '16 at 11:20
  • @harpreetSingh sorry, it's been a while since I've worked on this project. Do you get `nil` cell after overriding `-[UITableView insertRowsAtIndexPaths:withRowAnimation:]`? Anyway I'd recommend you to try approach from [updated upvoted answer](http://stackoverflow.com/a/14182059/1226304) and if it won't work - create test project only with this issue and share it here - someone including me might be able to check it and help you to fix this. – derpoliuk Jun 09 '16 at 10:32
  • I got a fix. Animation was not happening because i was calling [tableView reloadData] just after it.After removing that it is working like a charm.Thanks buddy – harpreetSingh Jun 10 '16 at 09:21
1

Use this:

-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
         [cell setFrame:CGRectMake(320, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];
            [UIView animateWithDuration:2 animations:^{
                [cell setFrame:CGRectMake(100, cell.frame.origin.y, cell.frame.size.width, cell.frame.size.height)];

            }];

}
mskw
  • 10,063
  • 9
  • 42
  • 64
  • 3
    the problem with this approach is that it will always reload the reusable cells with the animation when the user scrolls (not only for insertion). – David Robertson Sep 05 '15 at 12:41
  • @DavidRobertson you can keep the array of already animated cells in tableview or controller and check if the cell was displayed already, if not you can do animation of insertion. – Marek Staňa Mar 26 '19 at 11:39
0

Swift 3.0 version of Mattias Eriksson's Answer

func animate() {
    for cell in tblView.visibleCells {
        cell.frame = CGRect(x:320, y:cell.frame.origin.y, width:cell.frame.size.width, height:cell.frame.size.height)
            UIView.animate(withDuration: 1.0) {
                cell.frame = CGRect(x:0, y:cell.frame.origin.y, width:cell.frame.size.width, height:cell.frame.size.height)
            }
    }
}
Abhishek Jain
  • 4,557
  • 2
  • 32
  • 31