66

What I want to do is change the size of an UICollectionViewCell, and to animate that change, when the cell is selected. I already managed to do that unanimated by marking a cell as selected in collectionView: didSelectItemAtIndexPath: and then calling reloadData on my UICollectionView, displaying the selected cell with a different size.

Nevertheless, this happens all at once, and I have no clue how to get that change in size animated. Any ideas?

I already found Animate uicollectionview cells on selection, but the answer was to unspecific for me and I didn't figure out yet if it could also help me in my case.

Ignatius Tremor
  • 6,476
  • 4
  • 20
  • 25

9 Answers9

89

With the help of Chase Roberts, I now found the solution to my problem. My code basically looks like this:

// Prepare for animation

[self.collectionView.collectionViewLayout invalidateLayout];
UICollectionViewCell *__weak cell = [self.collectionView cellForItemAtIndexPath:indexPath]; // Avoid retain cycles
void (^animateChangeWidth)() = ^()
{
    CGRect frame = cell.frame;
    frame.size = cell.intrinsicContentSize;
    cell.frame = frame;
};

// Animate

[UIView transitionWithView:cellToChangeSize duration:0.1f options: UIViewAnimationOptionCurveLinear animations:animateChangeWidth completion:nil];

For further explanation:

  1. One of the most important step is the invalidateLayout call on the UICollectionView.collectionViewLayout you are using. If you forget it, it is likely to happen that cells will grow over the borders of your collection - that's something you most probably don't want to happen. Also consider that your code should at some point present the new size of your cell to your layout. In my case (I'm using an UICollectionViewFlowLayout), I adjusted the collectionView:layout:sizeForItemAtIndexPath method to reflect the new size of the modified cell. Forgetting that part, nothing will happen to your cell sizes at all if you first call invalidateLayout and then try to animate the change in size.

  2. Most strange (at least to me) but important is that you call invalidateLayout not inside the animation block. If you do so, the animation won't display smooth but glitchy and flickering.

  3. You have to call UIViews transitionWithView:duration:options:animations:completion: method with the cell as argument for transitionWithView, not the whole collection view, because it's the cell you want to get animated, not the entire collection.

  4. I used the intrinsicContentSize method of my cell to return the size I want - in my case, I grow and shrink the width of the cell to either display a button or hide it, depending on the selection state of the cell.

I hope this will help some people. For me it took several hours to figure out how to make that animation work correctly, so I try to free others from that time-consuming burden.

i_am_jorf
  • 51,120
  • 15
  • 123
  • 214
Ignatius Tremor
  • 6,476
  • 4
  • 20
  • 25
  • 3
    Nice job posting a follow up. – Chase Roberts Dec 12 '12 at 20:13
  • what is `actorCell` defined as? Should that be what you want to appear in the enlarged view? – Austin Feb 09 '13 at 20:24
  • I renamed the cell as "cellToChangeSize". The name to be found in my post before editing was just some remains of the copy-pasting out of my project, sorry for that. – Ignatius Tremor Feb 09 '13 at 21:01
  • "I adjusted the collectionView:layout:sizeForItemAtIndexPath method to reflect the new size of the modified cell." How did you do that? – Timuçin Jun 07 '13 at 12:07
  • 2
    The part about avoiding the retain cycle, shouldn't it rather be declared *outside* the block? And since you're using uicollectionview (which is iOS 6+), the preferred qualifier is `__weak` now. If `cell` appears *in* your block, a strong reference is created, so you're not avoiding any retain cycle. Nevertheless, thanks a lot for your follow up, very helpful! – matehat Jul 02 '13 at 02:22
  • 1
    Hey @matehat, I changed the code now to reflect your annotation. Glad to hear that you like the answer anyway :) – Ignatius Tremor Jul 30 '13 at 20:33
  • 1
    I just want to add here that you do not need to use __weak. A retain cycle generally occurs when your block retains some object that itself retains the block; i.e. going self.block = ^{self.someInteger++;}. Since your cell is not retaining the block, no retain cycles:) – Daniel Galasko Oct 01 '13 at 13:18
  • 3
    A little note of advice to those who read this: Using AutoLayout in your cells might break this. I had to change the height constraint of the cell instead, and then use [cell layoutIfNeeded] inside the animation block – Nailer Oct 04 '13 at 08:46
  • 1
    The cell change is animated but the collection view will jump to the new layout. – openfrog Oct 17 '13 at 11:23
  • Regarding @openfrog's comment, if you're trying to animate a change in height you'd need to use `transitionWithView:duration:options:animations:completion:` with the whole collection view as argument, right? – bcattle Feb 07 '14 at 02:12
  • 1
    This didn't work for me. I was able to change the size of the cell's frame, but nothing inside the cell changes size. I even tried modifying the individual subitems' frames, but they always stay the same. – Erika Electra May 22 '14 at 07:39
51

Animating the size of a cell works the same way for collection views as it does for table views. If you want to animate the size of a cell, have a data model that reflects the state of the cell (in my case, I just use a flag (CollectionViewCellExpanded, CollectionViewCellCollapsed) and return a CGSize accordingly.

When you call and empty performBathUpdates call, the view animates automatically.

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    [collectionView performBatchUpdates:^{
        // append your data model to return a larger size for
        // cell at this index path
    } completion:^(BOOL finished) {

    }];
}
Andy Poes
  • 1,552
  • 14
  • 19
  • 2
    Great answer. In my case, I just had to change the model, then call performBatchUpdates and within the block, call invalidateLayout. – Dorian Roy Feb 20 '13 at 15:29
  • I've got a custom layout that holds a transient property regarding which index path is a "standout" index path, and when I set that index path, the view redraws itself and moves the item at the index path towards the top of the collection, away from the rest of the items (kinda like Passbook). Setting that property and doing with @DorianRoy suggested was a huge help. – Ben Kreeger Jul 10 '13 at 18:15
  • 1
    This is far better solution especially considering that the changes are persisted after cell reuse. – dezinezync Apr 27 '14 at 16:25
  • Worked for me, using Swift 4.2 – jangelsb Mar 17 '20 at 21:54
31

I've had success animating changes by using -setCollectionViewLayout:animated: to create a new instance of the same collection view layout.

For example, if you're using a UICollectionViewFlowLayout, then:

// Make sure that your datasource/delegate are up-to-date first

// Then animate the layout changes
[collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
codeperson
  • 7,980
  • 4
  • 30
  • 49
  • This gives the smoothest animation by far. Great solution. – phatmann Jun 18 '14 at 00:20
  • None of the other answers worked for me—the others performed an animation on the cell, but the act of invalidating the layout (whether explicitly or behind the scenes) also caused issues with the placement of the other cells before or after the individual cell animation. This is the only answer that worked for me! – Evan R Aug 18 '15 at 00:45
  • This works except on the case (tested on iPhone4S, iOS8): during the animation time, if you scroll the CollectionView, the cell will get hidden. – chipbk10 Aug 10 '17 at 14:31
  • The downside is that the `contentOffset` changes. – Iulian Onofrei Jan 24 '19 at 11:00
22
[UIView transitionWithView:collectionView 
                  duration:.5 
                   options:UIViewAnimationOptionTransitionCurlUp 
                animations:^{

    //any animatable attribute here.
    cell.frame = CGRectMake(3, 14, 100, 100);

} completion:^(BOOL finished) {

    //whatever you want to do upon completion

}];

Play around with something along those lines inside your didselectItemAtIndexPath method.

Milad Faridnia
  • 8,038
  • 13
  • 63
  • 69
Chase Roberts
  • 8,262
  • 10
  • 64
  • 118
  • 1
    Playing around with that seems to have solved my problem. I'm right no refining my solution on the basis of your proposal for a bit more elegance in order to post it as soon as I'm done. Thank you very much so far. – Ignatius Tremor Dec 08 '12 at 18:23
  • 2
    It seems to work but the results are horrible visually. It seems to stutter to a halt. Any ideas? – Mike M Nov 11 '13 at 17:28
20

This is the simplest solution I have found - works like a charm. Smooth animation, and does not mess up the layout.

Swift

    collectionView.performBatchUpdates({}){}

Obj-C

    [collectionView performBatchUpdates:^{ } completion:^(BOOL finished) { }];

The blocks (closures in Swift) should be intentionally left empty!

Pavel Gurov
  • 5,467
  • 3
  • 23
  • 23
  • 1
    I've tried this solution with `reloadData`, `moveItemAtIndexPath: toIndexPath:`, `reloadItemsAtIndexPaths:` inside updates block, then realized that block should be left empty. Now really works like a charm. – zalogatomek Jun 23 '16 at 12:51
12

A good way to animate the cell size is to override isHighlighted in the collectionViewCell itself.

class CollectionViewCell: UICollectionViewCell {

    override var isHighlighted: Bool {
        didSet {
            if isHighlighted {
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: {
                    self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
                }, completion: nil)
            } else {
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut, animations: {
                    self.transform = CGAffineTransform(scaleX: 1, y: 1)
                }, completion: nil)
            }
        }
    }

}
Cristian Pena
  • 1,989
  • 16
  • 30
  • 1
    You said "A good way to animate the cell size is to override isSelected in the collectionViewCell itself." , but u override isHighlighted. These are 2 different states. – ShadeToD Jul 23 '19 at 15:02
10

Using performBatchUpdates will not animate the layout of the cell contents. Instead, you can use the following:

    collectionView.collectionViewLayout.invalidateLayout()

    UIView.animate(
        withDuration: 0.4,
        delay: 0.0,
        usingSpringWithDamping: 1.0,
        initialSpringVelocity: 0.0,
        options: UIViewAnimationOptions(),
        animations: {
            self.collectionView.layoutIfNeeded()
        },
        completion: nil
    )

This gives a very smooth animation.

This is similar to Ignatius Tremor's answer, but the transition animation did not work properly for me.

See https://github.com/cnoon/CollectionViewAnimations for a complete solution.

phatmann
  • 17,007
  • 6
  • 57
  • 47
  • Upvoted this answer, but it's still not perfect. Animation stretches cell before redrawing it again. But really animation started to be nice because height of the cell is animated too. – Ariel Bogdziewicz Sep 13 '19 at 13:51
2

This worked for me.

    collectionView.performBatchUpdates({ () -> Void in
        let ctx = UICollectionViewFlowLayoutInvalidationContext()
        ctx.invalidateFlowLayoutDelegateMetrics = true
        self.collectionView!.collectionViewLayout.invalidateLayoutWithContext(ctx)
    }) { (_: Bool) -> Void in
    }
eonil
  • 75,400
  • 74
  • 294
  • 482
0
class CollectionViewCell: UICollectionViewCell {
   override var isHighlighted: Bool {
       if isHighlighted {
            UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
                self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
            }) { (bool) in
                UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseInOut, animations: {
                    self.transform = CGAffineTransform(scaleX: 1, y: 1)
                }, completion: nil)
            }
        }
   }
}

One just needs to check if the isHighlighted variable was changed to true, then use UIView animation and animate back in the completion handler.

Alexander
  • 1,114
  • 12
  • 20