Axemasta
Axemasta

Reputation: 843

Don't animate UITableView cells that have already appeared

I have added some animation to a table view to make it look nicer when it loads. In the controller's viewDidLoad I make an asynchronous request for data and when it returns the table view is populated.

When my table loads the cells are revealed one by one. (I took inspiration from this excellent guide).

- (void)tableFadeInAnimation {
    //[_venueTableView reloadData];

    NSArray<UITableViewCell *> *cells = _venueTableView.visibleCells;

    NSInteger index = 0;

    for (UITableViewCell * m in cells){
        UITableViewCell *cell = m;
        cell.alpha = 0;
        [UIView animateWithDuration:1 delay:0.25 * index options:0 animations:^(){
            cell.alpha = 1;
        } completion:nil];
        NSLog(@"end of table animation");
        index += 1;
    }
}

My problem with running this as an initialising function is that once this finishes my table has no more animations to perform. I then took this principle to cellForRowAtIndexPath (removing the loop).

cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
        cell.alpha = 1;
    }];

This would load all the cells together but would animate new cells appearing on the table.

cell.alpha = 0;

[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
    cell.alpha = 1;
} completion:^(BOOL finished){
    NSLog(@"animation complete");
}];

This made the table load each cell 1 by 1 however it is tied to all the cells (not the visible ones) so the further you go down the table, the longer the loading time for the cell.

Also when you move back up the table, all the older cells reanimate onto the table. I want the old cells to remain and the new cells to animate. Is there a way I can keep track of which cells have been loaded and only animate brand new, never before seen cells?

Upvotes: 1

Views: 404

Answers (4)

Axemasta
Axemasta

Reputation: 843

Thanks to @trungduc for the answer, I'm posting the completed solution to this in the hopes people will find it useful. To stop the table drawing cells that have already appeared you need to implement a variable to track the maximum index that has been displayed on the table, lastCellDisplayedIndex. In @trungduc's answer he put this variable in the - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method however I found this created some errors making some cells redraw themselves. I had a read up on the difference between the cellForRow and cellWillDisplay methods and it seemed like the best place to put the animation was cellWillDisplay as the cell has been initialised and is apparently the place you should be performing UI tweaks to a cell (like animations!).

This method looks like this:

-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{

    lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
    NSLog(@"lastCellDisplayedIndex = %ld, indexPath for Cell = %ld", lastCellDisplayedIndex, indexPath.row);

    if (lastCellDisplayedIndex <= indexPath.row){
        cell.alpha = 0;
        [UIView animateWithDuration:2.0 animations:^(){
            cell.alpha = 1;
        }];
        if (lastCellDisplayedIndex == totalCellsToDisplay - 1){
            NSLog(@"END OF TABLE ANIMATIONS!");
            lastCellDisplayedIndex = totalCellsToDisplay + 1;
        }
    }
    else {
        cell.alpha = 1;
    }

}

This method handles almost everything. It will first change the value of lastCellDisplayedIndex to the value of the max index the table has seen. Next it will decide whether the cell it is handling should be animated or left as is. I also had to add a guard variable (of sorts), totalCellsToDisplay would act as your tables datasource array: -

(NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return totalCellsToDisplay;
}

So in your real app you would instead have

- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return yourTableDataArray.count;
}

The reason I am checking the maximum number of cells being drawn is if you just have this code:

lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);

if (lastCellDisplayedIndex <= indexPath.row){}

then the maximum index will never go higher than the final cell, so this cell will be reanimated every-time you scroll up and down. To fix this when the indexPath = the total cells - 1 (because of zero index) then you bump the value of lastCellDisplayedIndex up so that no more cells will ever get drawn.

Finally we need to solve the issue of how many cells the table will initially draw. I'm not sure quite how this works but in my testing it would always draw 15 cells (if I returned more than 15). Anyway I implemented both my staggered load animation and fixed this problem with my loading animation function.

- (void)tableFadeInAnimation {
    [_myTable reloadData];

    NSArray<UITableViewCell *> *cells = _myTable.visibleCells;

    NSInteger index = 0;

    for (UITableViewCell * m in cells){
        UITableViewCell *cell = m;
        cell.alpha = 0;
        [UIView animateWithDuration:0.5 delay:0.25 * index options:0 animations:^(){
            cell.alpha = 1;
        } completion:^(BOOL finished){
            lastCellDisplayedIndex = _myTable.visibleCells.count;
            NSLog(@"Table Animation Finished, lastCellDisplayed Index = %ld", lastCellDisplayedIndex);
        }];
        NSLog(@"end of table animation");
        index += 1;
    }
}

I used the completion block of the function to set the value of lastCellDisplayed equal to the number of cells that are visible. Now the table view will animate all new cells.

Hope this helps and thanks to @trungduc for the answer!

Upvotes: 0

Connor Neville
Connor Neville

Reputation: 7361

My recommended approach, assuming you have some backing data source to provide data in cellForRowAtIndexPath, is to add some mutable property hasBeenDisplayed to your model objects (or an NSDictionary that maps each model object to a bool that indicates whether or not it has been displayed). This logic is complicated enough that you want some logic code to ensure the consistency of the view code. Then, once you have this property, you can call your custom animation in cellForRowAtIndexPath if the cell has not yet been displayed.

Upvotes: 0

MiKL
MiKL

Reputation: 1850

The best approach for this is to add your animation block and any change to your cell's frame or alpha, in the tableView:willDisplayCell:forRowAtIndexPath: method of your UITableViewDelegate

Upvotes: 0

trungduc
trungduc

Reputation: 12154

You should have a property to keep track index of last cell which is displayed (name lastCellDisplayedIndex). Only animate cells which have index less than lastCellDisplayedIndex. Each time call reloadData, reset lastCellDisplayedIndex = -1.

Try my below code.

@property (nonatomic, assign) NSUInteger lastCellDisplayed;

- (void)viewDidLoad {
  [super viewDidLoad];

  [self reloadTableView];
}

// Use this method each time you want to reload data of tableView 
// instead of |reloadData| method
- (void)reloadTableView {
  _lastCellDisplayedIndex = -1;
  [self.tableView reloadData];
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
  // Update |_lastCellDisplayedIndex| each time a cell is displayed
  _lastCellDisplayedIndex = MAX(indexPath.row, _lastCellDisplayedIndex);
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  ...
  // Only animate cells which have |indexPath.row| < |_lastCellDisplayedIndex|
  if (_lastCellDisplayedIndex < indexPath.row) {
    cell.alpha = 0;

    [UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
      cell.alpha = 1;
    } completion:^(BOOL finished){
      NSLog(@"animation complete");
    }];
  } else {
    cell.alpha = 1;
  }
  ...
}

Upvotes: 1

Related Questions