Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weird animation behaviour with UICollectionView (insert & delete item) [closed]

When inserting or deleting an item from a UICollectionView, it seems like an extra cell appears during the animation, and this extra cell moves in the wrong direction. I've tried exactly the same with a UITableView, and there is no problem.

A video of the problem is here: https://dl.dropbox.com/u/11523469/CollectionViewBug.mov, with the collection view on the left and the table view on the right. The number in each cell is the indexPath.item value of the cell when the cell is created.

The problem is first noticeable in the video between 0:08 and 0:12 (insert) and then again at 0:16 to 0:20 (delete).

The project is available here: https://dl.dropbox.com/u/11523469/CollectionViewBug.zip

i.e. when inserting a cell, all cells below where the cell is being inserted move down to make room for the new cell. But this extra cell appears and overlaps the others and moves up.

Likewise, when deleting a cell, all cells below the cell being deleted move up to fill the gap where the cell used to be. But this extra cell appears and overlaps the others and moves down.

The first action to be performed on the collection view, either insert or delete, does not cause this problem. But on all subsequent actions the problem is there.

Has anyone else experienced the same problem with UICollectionView? Does anyone have a solution or workaround?

Thanks!

like image 293
Sam Avatar asked Jan 24 '13 16:01

Sam


1 Answers

I came up with a workaround which seems to fix the problem but is very specific to the example provided. My guess is that when cells are reused they have the wrong starting point which causes the weird animations.

I changed the Storyboard to use a subclass of UICollectionViewFlowLayout:

// MyFlowLayout - subclass of UICollectionViewFlowLayout

#import "MyFlowLayout.h"

@interface MyFlowLayout ()

@property (strong) NSMutableArray *deleteIndexPaths;
@property (strong) NSMutableArray *insertIndexPaths;
@property (assign) float rowOffset;

@end

@implementation MyFlowLayout

-(id)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        // minimumInteritemSpacing may be adjusted upwards but this example ignores that
        self.rowOffset = self.itemSize.height + self.minimumInteritemSpacing;
    }

    return self;
}

// As per Mark Pospesel corrections to CircleLayout

- (void)prepareForCollectionViewUpdates:(NSArray *)updateItems
{
    // Keep track of insert and delete index paths
    [super prepareForCollectionViewUpdates:updateItems];

    self.deleteIndexPaths = [NSMutableArray array];
    self.insertIndexPaths = [NSMutableArray array];

    for (UICollectionViewUpdateItem *update in updateItems)
    {
        if (update.updateAction == UICollectionUpdateActionDelete)
        {
            [self.deleteIndexPaths addObject:update.indexPathBeforeUpdate];
        }
        else if (update.updateAction == UICollectionUpdateActionInsert)
        {
            [self.insertIndexPaths addObject:update.indexPathAfterUpdate];
        }
    }
}

- (void)finalizeCollectionViewUpdates
{
    [super finalizeCollectionViewUpdates];
    // release the insert and delete index paths
    self.deleteIndexPaths = nil;
    self.insertIndexPaths = nil;
}

// The next two methods have misleading names as they get called for all visible cells on     both insert and delete

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    // Must call super
    UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
    if (!attributes)
        attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    if ([self.insertIndexPaths containsObject:itemIndexPath]) {
        // Initial position for an inserted cell is it's final position - fades in
        CGRect frame = attributes.frame;
        frame.origin.y = itemIndexPath.row * self.rowOffset;
        attributes.frame = frame;
        attributes.zIndex = -1; // stop the inserted cell bleeding through too early in the animation
    }
    if ([self.deleteIndexPaths count]) {
        NSIndexPath *deletedPath = self.deleteIndexPaths[0];  // Might be more than one but this example ignores that
        if (itemIndexPath.row > deletedPath.row) {
            // Anything after the deleted cell needs to slide up from the position below it's final position
            // Anything before the deleted cell doesn't need adjusting
            CGRect frame = attributes.frame;
            frame.origin.y = ((itemIndexPath.row + 1) * self.rowOffset);
            attributes.frame = frame;
        }
    }

    return attributes;
}

- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath];
    if (!attributes)
        attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];

    // I would have expected the final positions to already be correct but my guess is that re-used cells
    // are not considered until after the animation block settings have been generated
    CGRect frame = attributes.frame;
    frame.origin.y = itemIndexPath.row * self.rowOffset;
    attributes.frame = frame;

    if ([self.deleteIndexPaths containsObject:itemIndexPath]) {
        // Fade out the deleted cell
        attributes.alpha = 0.0;
    }

    return attributes;
}

@end
like image 142
Gareth Avatar answered Oct 08 '22 16:10

Gareth