42

I have a UICollectionViewController that using a standard UICollectionViewFlowLayout to display a single vertical column of cells. I am attempting to create an expand/collapse animation on a cell when a cell is tapped. I use the following code to accomplish this:

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath (NSIndexPath *)indexPath
{
    [self.feedManager setCurrentlySelectedCellIndex:indexPath.item];
    [self.collectionView performBatchUpdates:nil completion:nil];
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    //Return the size of each cell to draw
    CGSize cellSize = (CGSize) { .width = 320, .height = [self.feedManager heightForCellAtIndexPath:indexPath] };
    return cellSize;
}

The 'selectedCellIndex' property on my managing object tells the data model to return an expanded or collapsed size inside of heightForCellAtIndexpath:

[self.collectionView performBatchUpdates:nil completion:nil];

Then the performBatchUpdates:completiong: method animates this size change nicely. However! When the animation happens, the expanding cell may cause a partially visible cell at the bottom of the screen to go off the screen.

If this is the case and I subsequently collapse this cell, the now off screen cell will snap to its old position without animation, while all of the other visible cells will animate as desired. My intuition says that this is the correct behaviour since the cell is off screen when the collapse animation is being performed, and is not being included in the animation rendering process. My question becomes, how do I prevent this from happening?

I would prefer all of the cells to animate together regardless if they are off screen or not. Any thoughts?

Prasad Devadiga
  • 2,573
  • 20
  • 43
Cory Breed
  • 423
  • 1
  • 4
  • 8
  • I found a very similar issue when poking around http://stackoverflow.com/questions/13698275/uicollectionview-moveitematindexpathtoindexpath-issues-moving-items-not-on-scr – Cory Breed Jun 05 '13 at 02:08
  • It seems the reuseable cells are getting in the way of the animation since the offscreen cell is getting flagged as ready for reuse. Any thoughts on how to prevent a cell from getting flagged for reuse? – Cory Breed Jun 05 '13 at 02:15
  • I have exactly the same problem and would really like a solution, even if that solution is a radar number! – jrturton Jun 12 '13 at 18:38
  • 3
    I'd suggest rolling your own layout class for this. Flow layout animations are too buggy in my experience, particularly for cells crossing the `layoutAttributesForElementsInRect` boundary. I spent days attempting to work around the bugs by overriding the animation-related delegate methods, but eventually got much better results by implementing my own layout class. – Timothy Moose Jun 14 '13 at 16:44
  • @TimothyMoose care to share some details in an answer? – jrturton Jun 16 '13 at 07:13
  • @jrturton I open sourced my grid layout and provided a working example project (see updated answer). – Timothy Moose Jun 19 '13 at 05:31

1 Answers1

29

This is a UICollectionViewFlowLayout bug, but there is a workaround. The problem is that the attributes returned by initialLayoutAttributesForAppearingItemAtIndexPath: have the wrong frames. Specifically, they have the frames of the final, on screen position rather than the initial, off screen position. You just need to override this method and return correct frames. The basic structure of the override would look something like this:

- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath
{
    UICollectionViewLayoutAttributes *pose = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
    if (<test for incorrect frame>) {
        CGRect frame = pose.frame;
        frame.origin.y = <calculate correct frame>;
        pose.frame = frame;
    }
    return pose;
}

You will be responsible for identifying scenarios where the frames need to be adjusted, which may involve keeping your own internal state. I haven't worked through this logic, but I did do a crude test and was able to get smooth animation.

Generalizing a bit, it is my experience that there UICollectionViewFlowLayout is so buggy and there are so many corner cases to contend with if you've got items moving on and off screen combined with any inserts, deletions, or moves, that I've found it easier to roll my own simple layouts. If you're not going to do any inserts, deletions, or moves, then overriding UICollectionViewFlowLayout may well be your best bet.

Let me know if you need more help.

EDIT

If you're interested in looking at a 3-rd party layout, I open sourced my custom grid layout VCollectionViewGridLayout and added an example project demonstrating smooth expanding cell height. Try running the Expand project. There is also a Sort & Filter project with animated sorting and filtering. Both projects let you toggle between flow layout and grid layout so you can see the improvement.

Timothy Moose
  • 9,895
  • 3
  • 33
  • 44
  • Your tip about the wrong frame in `initialLayout...` was spot on. I'm storing attributes for cells later than my selected cell, and comparing them to the frames returned in that method, replacing where necessary. My existing layout subclass does plenty of work already but I'll take a look at yours. – jrturton Jun 19 '13 at 06:15
  • @TimothyMoose how am I supposed to include VCollectionViewLayout and TLIndexPathTools inside my xcode project? I just added VCollectionViewLayout.h, VCollectionViewLayout.m files and drag/drop TLIndexPathTools project file inside my project's Frameworks group and added it as Dependency and Linked Framework in Build Phases, but I get the following error: Undefined symbols for architecture i386: "_OBJC_CLASS_$_NSManagedObject", referenced from: objc-class-ref in libTLIndexPathTools.a(TLIndexPathDataModel.o) – ftartaggia Aug 21 '13 at 23:51
  • Thanks for asking. I just realized I accidentally removed the TLIndexPathTools install instructions from the [readme](https://github.com/wtmoose/TLIndexPathTools). It sounds like you've done everything except link to CoreData and QuartzCore frameworks. – Timothy Moose Aug 22 '13 at 00:01
  • actually I managed to build my project by just adding both TLIndexPathTools and VCollectionViewGridLayout to Linked Frameworks. Now, the weird thing is this: if I assign VCollectionViewGridLayout to my CollectionView via Storyboard everything is fine, but as soon as I add this line of code: "VCollectionViewGridLayout *layout = [[VCollectionViewGridLayout alloc] init];" I get the error: Undefined symbols for architecture i386: "_OBJC_CLASS_$_NSManagedObject", referenced from: objc-class-ref in libTLIndexPathTools.a(TLIndexPathDataModel.o) – ftartaggia Aug 22 '13 at 00:23
  • and as you suggested, adding CoreData framework seems to fix the problem... I cannot figure out what's going on – ftartaggia Aug 22 '13 at 00:26
  • It won't run without CoreData because the `TLIndexPathSectionInfo` class is an implementation of the `NSFetchedResultsSectionInfo` protocol and the main component of TLIndexPathTools, `TLIndexPathDataModel`, uses this class to organize data into sections. – Timothy Moose Aug 22 '13 at 00:29