Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animate UICollectionView frame change when inserting cells

I want to change the frame size of a UICollectionView in an animation that runs alongside an animated cell insertion to the same collection view inside a performBatchUpdates:completion: block.

This is the code that triggers the cell insertion:

[collectionView performBatchUpdates:^{
    indexPathOfAddedCell = ...;
    [collectionView insertItemsAtIndexPaths:@[ indexPathOfAddedCell ]];
} completion:nil];

Because the cell insertion causes the collection view's contentSize to change, I tried KVO-registering for changes to that property and then trigger the collection view frame update from the KVO handler.

The problem with that approach is that the KVO trigger for contentSize fires too late: the cell insertion animation has already completed at that time (actually, KVO triggers right before the completion handler of performBatchUpdates:completion: gets called but after the animation has played out in the UI).

Iʼm not using auto layout.

Edit: I put a sample project to demonstrate my problem on GitHub.

Edit 2: I should mention that I need this for a component Iʼm writing (OLEContainerScrollView) that is supposed to be 100% independent of the collection view. Because of this, I cannot subclass the collection view layout, nor do I have influence over the code that triggers the cell animations. Ideally, a solution would also work for UITableView, which exhibits the same behavior.

like image 263
Ole Begemann Avatar asked Jun 24 '14 18:06

Ole Begemann


2 Answers

I looked at how both collection views and table views update their content inset, and indeed, the scroll view's content size is only updated after the animation completes in both cases. There doesn't seem to be a really good method of listening to the future content size without using private API, but it is possible.

For table views, the trigger for animation start is -[UITableView _endCellAnimationsWithContext:]. This method sets up all the animations needed (for the future visible cells only), executes them and sets a completion block which eventually calls -[UITableView _updateContentSize]. _updateContentSize uses the internal -[UITableView _contentSize] method to set the correct scroll view content size. Since _endCellAnimationsWithContext: deals with animations only, the data behind the table view is already updated, so calling _contentSize (or using valueForKey:@"_contentSize") returns a correct size.

It is very similar for collection views. The trigger is -[UICollectionView _endItemAnimations], it starts many animations for each cell, header and footer, and when all the animations finish, -[UICollectionView _updateAnimationDidStop:finished:context:] sets the correct content size. Because this is a collection view, its collection view layout actually knows the target content size, so you can call -[UICollectionViewLayout collectionViewContentSize] to get the updated content size.

None of these are really good options for use on the app store. One option I can think of is ISA-swizzling each scroll view subclass added, and wrapping all animatable entry points, tracking whether they are batched or not, and either at the end of the batch or at the end of the standalone animated operation, use the corresponding methods (-[UITableView _contentSize] and -[UICollectionViewLayout collectionViewContentSize]) to get the target content size.


Original answer, in case you want to hear about changes to a collection view size in your own collection views:

Subclass the collection view layout (if you haven't already), and using either notification center or a delegate method, notify on prepareForAnimatedBoundsChange: for other animations. They will be added to the animation block.

From the documentation:

You can also use this method to perform additional animations. Any animations you create are added to the animation block used to handle the insertions, deletions, and bounds changes.

You may need to determine what the changes are, and only notify about insertion animations.

like image 98
Léo Natan Avatar answered Nov 12 '22 12:11

Léo Natan


I've looked into your demo project and I think that there is no need of KVO. If you want to change collection view's frame with animation while inserting new cell, then I think you can do something like this:

#import "ViewController.h"

@interface ViewController () <UICollectionViewDataSource, UICollectionViewDelegate>

@property (weak, nonatomic) IBOutlet UICollectionView *collectionView;
@property (weak, nonatomic) IBOutlet UIView *otherView;
@property (nonatomic) NSInteger numberOfItemsInCollectionView;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.numberOfItemsInCollectionView = 1; // This is our model
}

- (IBAction)addItem:(id)sender
{
    // Change the model
    self.numberOfItemsInCollectionView += 1;

    [UIView animateWithDuration:0.24 animations:^{
        NSIndexPath *indexPathOfInsertedCell = [NSIndexPath indexPathForItem:self.numberOfItemsInCollectionView - 1 inSection:0];
        [self.collectionView insertItemsAtIndexPaths:@[ indexPathOfInsertedCell ]];

        CGRect collectionViewFrame = self.collectionView.frame;
        collectionViewFrame.size.height = (self.numberOfItemsInCollectionView * 40) + 94;
        self.collectionView.frame = collectionViewFrame;
    }];

}

#pragma mark - UICollectionViewDataSource

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.numberOfItemsInCollectionView;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
{
    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
    return cell;
}

@end

Or if you want fade effect:

- (IBAction)addItem:(id)sender
{
    // Change the model
    self.numberOfItemsInCollectionView += 1;

    CATransition *transition = [CATransition animation];
    transition.type = kCATransitionFade;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    transition.fillMode = kCAFillModeForwards;
    transition.duration = 0.5;

    [[self.collectionView layer] addAnimation:transition forKey:@"UICollectionViewInsertRowAnimationKey"];
    NSIndexPath *indexPathOfInsertedCell = [NSIndexPath indexPathForItem:self.numberOfItemsInCollectionView - 1 inSection:0];
    [self.collectionView insertItemsAtIndexPaths:@[ indexPathOfInsertedCell ]];

    CGRect collectionViewFrame = self.collectionView.frame;
    collectionViewFrame.size.height = (self.numberOfItemsInCollectionView * 40) + 94;
    self.collectionView.frame = collectionViewFrame;
}

I've checked this with your demo project, and its works for me. Please try this and comment if it works for you too.

like image 33
arturdev Avatar answered Nov 12 '22 13:11

arturdev