SkillAgentSearch skills...

CollectionViewAnimations

Sample project demonstrating how to expand / collapse collection view cells using custom animation blocks.

Install / Use

/learn @cnoon/CollectionViewAnimations
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Collection View Animations

Sample project demonstrating how to expand / collapse collection view cells using UIView animation closures in addition to sticky headers.

Features

  • [X] Collection View with Custom Layout
  • [X] Custom Layout Attributes supporting UIView Cell Content Size Animations
  • [X] Content Offset Scroll Behavior while Animating
  • [X] Sticky Section Headers in Collection View
  • [X] Efficient Sticky Header Performance using Invalidation Context

Description

Collection views are very powerful, but can be cumbersome as well. When initially trying to figure out how to expand / collapse a cell in a UICollectionView, I was very surprised to find so little documentation around using a typical UIView.animationWithDuration closure to control the animation. The majority of documentation I found was using either the setCollectionViewLayout(_:animated:) API or the performBatchUpdates(_:completion:) API. However, neither of these approaches give you full control of the actual animation. I simply wanted to be able to use my own animation closures to control the behavior of the cells using a custom UICollectionViewLayout.

After much investigation, I was able to find a solution leveraging all the UICollectionViewLayout APIs. You can animate the cells using a typical UIView.animationWithDuration closure to control all aspects of the animation in combination with the custom layout. You can even control the contentOffset of the UICollectionView while animating. Once I could control the animation, I needed to add sticky headers into the collection view as well. That proved to be MUCH more difficult and required a custom UICollectionViewLayoutInvalidationContext.

This sample project was created to demonstrate how to set up a fully custom UICollectionViewLayout to perform custom cell animations as well as demonstrate how to efficiently implement sticky section headers. Hopefully this will help someone else when facing the same challenges.


Apps

The sample project contains two different applications, each demonstrating different functionality alongside a custom layout.

Cell Animations

The Cell Animations app target demonstrates how to implement a custom UICollectionViewLayout in a way that can support custom cell expand and collapse animations. Each time a cell is tapped, it is expanded and brought fully on-screen if it is collapsed, and is collapsed if it is expanded.

func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
    layout.selectedCellIndexPath = layout.selectedCellIndexPath == indexPath ? nil : indexPath

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

Try modifying the values to customize the feel of the animation. This gives you the ultimate flexibility to control all aspects of the animation in any way you wish.

Preparing the Layout

The first step to implementing a custom UICollectionViewLayout is to compute the attributes for all cells that need to be displayed in the collection view.

override func prepareLayout() {
    super.prepareLayout()

    previousAttributes = currentAttributes

    contentSize = CGSizeZero
    currentAttributes = []

    if let collectionView = collectionView {
        let itemCount = collectionView.numberOfItemsInSection(0)
        let width = collectionView.bounds.size.width
        var y: CGFloat = 0

        for itemIndex in 0..<itemCount {
            let indexPath = NSIndexPath(forItem: itemIndex, inSection: 0)
            let attributes = UICollectionViewLayoutAttributes(forCellWithIndexPath: indexPath)
            let size = CGSize(
                width: width,
                height: itemIndex == selectedCellIndexPath?.item ? 300.0 : 100.0
            )

            attributes.frame = CGRectMake(0, y, width, size.height)

            currentAttributes.append(attributes)

            y += size.height
        }

        contentSize = CGSizeMake(width, y)
    }
}

As you can see in the prepareLayout implementation, new cell attributes are created for each cell. Each cell is placed directly below the previous cell, and the selected cell is 3 times larger than non-selected cells. The total height of all the cells is used to populate the contentSize.

The trick here is that the previousAttributes are being stored. You'll see why that's important here in a bit.

Invalidating the Layout

For this particular collection view, we only want to invalidate the layout if the new bounds rect has a different size. We can ignore all origin changes in the bounds because this collection view doesn't need to react to them. This is very important from a performance standpoint.

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    if let oldBounds = collectionView?.bounds where !CGSizeEqualToSize(oldBounds.size, newBounds.size) {
        return true
    }

    return false
}

You should never invalidate the layout unless you absolutely have to. Invalidating the layout will cause your entire layout to be recalculated which can have serious performance implications.

Layout Attributes

The next step to implementing the custom layout is to override both of the following methods:

override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return currentAttributes.filter { CGRectIntersectsRect(rect, $0.frame) }
}

override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
    return currentAttributes[indexPath.item]
}

The first method is used by the collection view to query the layout for all attributes within a given rect. This is generally used to query for all the visible cell layout attributes. Therefore, this implementation simply filters the current attributes that intersect the specified rect. Since the attributes were already computed in the prepareLayout method, this implementation is very straightforward.

The second method is used by the collection view to get the attributes for a given index path. Again, since this information was computed in the prepareLayout method, we only need to return the layout attributes for the specified index path.

Initial Layout Attributes

Overriding the initial layout attributes API is where things start to get interesting.

override func initialLayoutAttributesForAppearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes {
    return previousAttributes[itemIndexPath.item]
}

This is why the previousAttributes are stored in the prepareLayout method. When the collection view runs an animation, it needs to know the initial attributes and the layout attributes for the end of the animation. By default, Apple provides initial attributes that result in a fade-in animation. If you want to have fine-grained control over the animation, you need to provide the initial layout attributes for each cell to tell the collection view exactly how the animation should occur.

To see this behavior in action, comment out the initialLayoutAttributesForAppearingItemAtIndexPath method in the Cell Animations app and watch what happens. It isn't pretty...

Final Layout Attributes

Overriding the final layout attributes API is just as important as the initial layout attributes, but is slightly easier.

override func finalLayoutAttributesForDisappearingItemAtIndexPath(itemIndexPath: NSIndexPath) -> UICollectionViewLayoutAttributes? {
    return layoutAttributesForItemAtIndexPath(itemIndexPath)
}

By default, Apple will fade-out a cell that is disappearing. Since this is not the desired behavior, you need to provide the layout attributes for this case. For the Cell Animations app, the cell should animate to the final position without changing the alpha value. To accomplish this, the current layout attributes for the specified index path can be returned.

Try commenting out this method in the Cell Animations app to observe how this affects the animations.

Content Offset

Now that the cells are expanding and collapsing, we need to be able to scroll the collection view during the animation to make sure the cell is completely on-screen when expanded. Thankfully, Apple has a way for us to override the default scrolling behavior in these situations.

override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint) -> CGPoint {
    guard let selectedCellIndexPath = selectedCellIndexPath else { return proposedContentOffset }

    var finalContentOffset = proposedContentOffset

    if let frame = layoutAttributesForItemAtIndexPath(selectedCellIndexPath)?.frame {
        let collectionViewHeight = collectionView?.bounds.size.height ?? 0

        let collectionViewTop = proposedContentOffset.y
        let collectionViewBottom = collectionViewTop +
View on GitHub
GitHub Stars259
CategoryDevelopment
Updated3mo ago
Forks21

Languages

Swift

Security Score

92/100

Audited on Jan 5, 2026

No findings