Strange animation behavior using UICollectionView (insert & delete element)

When you insert or remove an element from the UICollectionView, it seems that during the animation an extra cell appears and this extra cell moves in the wrong direction. I tried exactly the same with the UITableView and there are no problems.

A video of the problem is here : https://dl.dropbox.com/u/11523469/CollectionViewBug.mov , with the image of the collection on the left and a 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 from 0:08 to 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 the cells below where the cell is inserted are moved down to make room for the new cell. But this extra cell appears and overlaps with others and moves up.

Similarly, when a cell is deleted, all cells below the deleted cell are moved up to fill the gap in which the cell was. But this extra cell appears and overlaps the rest and moves down.

The first action that you must perform in the collection view, either insert or delete, does not cause this problem. But with all subsequent actions there is a problem.

Has anyone else had the same issue with UICollectionView? Does anyone have a solution or workaround?

Thanks!

+7
source share
3 answers

I came up with a workaround that seems to fix the problem, but is very specific to the above example. I guess when cells are reused, they have the wrong starting point, which causes strange 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 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 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 
+10
source

If someone came here looking for MonoTouch's answer, here's what I got by translating Gareth's answer .

The base class that defines EnableAnimationFix and two virtual methods: ApplyAnimationFixToAppearingItem and ApplyAnimationFixToDisappearingItem .

 public class CollectionViewFlowLayout : UICollectionViewFlowLayout { protected List<int> _insertedItems = new List<int> (); protected List<int> _deletedItems = new List<int> (); protected virtual bool EnableAnimationFix { get { return false; } } protected virtual void ApplyAnimationFixToAppearingItem (int index, UICollectionViewLayoutAttributes attrs) { throw new NotImplementedException (); } protected virtual void ApplyAnimationFixToDisappearingItem (int index, UICollectionViewLayoutAttributes attrs) { throw new NotImplementedException (); } public override UICollectionViewLayoutAttributes InitialLayoutAttributesForAppearingItem (NSIndexPath path) { var attrs = base.InitialLayoutAttributesForAppearingItem (path); if (!EnableAnimationFix) { return attrs; } attrs = attrs ?? LayoutAttributesForItem (path); if (attrs != null) ApplyAnimationFixToAppearingItem (path.Row, attrs); return attrs; } public override UICollectionViewLayoutAttributes FinalLayoutAttributesForDisappearingItem (NSIndexPath path) { var attrs = base.FinalLayoutAttributesForDisappearingItem (path); if (!EnableAnimationFix) { return attrs; } if (attrs == null && _deletedItems.Contains (path.Row)) { // Calling LayoutAttributesForItem will cause an exception so we return now. // I think this happens when last and only item is deleted, and there are no other cells in cell pool. return null; } attrs = attrs ?? LayoutAttributesForItem (path); if (attrs != null) ApplyAnimationFixToDisappearingItem (path.Row, attrs); return attrs; } public override void PrepareForCollectionViewUpdates (UICollectionViewUpdateItem [] updateItems) { base.PrepareForCollectionViewUpdates (updateItems); if (!EnableAnimationFix) return; _insertedItems.Clear (); _deletedItems.Clear (); foreach (var update in updateItems) { if (update.UpdateAction == UICollectionUpdateAction.Insert) { _insertedItems.Add (update.IndexPathAfterUpdate.Row); } else if (update.UpdateAction == UICollectionUpdateAction.Delete) { _deletedItems.Add (update.IndexPathBeforeUpdate.Row); } } } public override void FinalizeCollectionViewUpdates () { base.FinalizeCollectionViewUpdates (); if (!EnableAnimationFix) return; _insertedItems.Clear (); _deletedItems.Clear (); } } 

And here is my actual collection view layout code:

 public class DraftsLayout : CollectionViewFlowLayout { // ... protected override bool EnableAnimationFix { get { return true; } } protected override void ApplyAnimationFixToAppearingItem (int index, UICollectionViewLayoutAttributes attrs) { if (_insertedItems.Contains (index)) { SetXByIndex (attrs, index); attrs.ZIndex = -1; } int deletedToTheLeft = _deletedItems.Count (i => i < index); if (deletedToTheLeft > 0) { SetXByIndex (attrs, index + deletedToTheLeft); } } protected override void ApplyAnimationFixToDisappearingItem (int index, UICollectionViewLayoutAttributes attrs) { SetXByIndex (attrs, index); if (_deletedItems.Contains (index)) { attrs.Alpha = 0; } } const int SnapStep = 150; static void SetXByIndex (UICollectionViewLayoutAttributes attrs, int index) { var frame = attrs.Frame; frame.X = index * SnapStep; attrs.Frame = frame; } } 

Note that this code should potentially handle multiple deletions in a package nicely.
Kudos to Gareth.

+1
source

I have performed all kinds of odd animation behavior related to using reloadData or reloadItemsAtIndexPaths. When I do not use these methods, the animation behavior seems to work as advertised.

0
source

All Articles