Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionViewCell - contents do not animate alongside cell's contentView

Problem looks like this: http://i.imgur.com/5iaAiGQ.mp4 (red is a color of cell.contentView)

Here is the code: https://github.com/nezhyborets/UICollectionViewContentsAnimationProblem

Current status: The content of UICollectionViewCell's contentView does not animate alongside contentView frame change. It gets the size immediately without animation.

Other issues faced when doing the task: The contentView was not animating alongside cell's frame change either, until i did this in UICollectionViewCell subclass:

override func awakeFromNib() {
    super.awakeFromNib()

    //Because contentView won't animate along with cell
    contentView.frame = bounds
    contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}

Other notes: Here is the code involved in cell size animation

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    self.selectedIndex = indexPath.row

    collectionView.performBatchUpdates({
        collectionView.reloadData() 
    }, completion: nil)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let isSelected = self.selectedIndex == indexPath.row
    let someSize : CGFloat = 90 //doesn't matter
    let sizeK : CGFloat = isSelected ? 0.9 : 0.65
    let size = CGSize(width: someSize * sizeK, height: someSize * sizeK)

    return size
}

I get the same results when using collectionView.setCollectionViewLayout(newLayout, animated: true), and there is no animation at all when using collectionView.collectionViewLayout.invalidateLayout() instead of reloadData() inside batchUpdates.

UPDATE When I print imageView.constraints inside UICollectionView's willDisplayCell method, it prints empty array.

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    for view in cell.contentView.subviews {
        print(view.constraints)
    }

    //Outputs
    //View: <UIImageView: 0x7fe26460e810; frame = (0 0; 50 50); autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x608000037280>>
    //View constraints: []
}
like image 871
Oleksii Nezhyborets Avatar asked Nov 18 '16 15:11

Oleksii Nezhyborets


3 Answers

This is a finicky problem, and you're very close to the solution. The issue is that the approach to animating layout changes varies depending on whether you're using auto layout or resizing masks or another approach, and you're currently using a mix in your ProblematicCollectionViewCell class. (The other available approaches would be better addressed in answer to a separate question, but note that Apple generally seems to avoid using auto layout for cells in their own apps.)

Here's what you need to do to animate your particular cells:

  1. When cells are selected or deselected, tell the collection view layout object that cell sizes have changed, and to animate those changes to the extent it can do so. The simplest way to do that is using performBatchUpdates, which will cause new sizes to be fetched from sizeForItemAt, and will then apply the new layout attributes to the relevant cells within its own animation block:

    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.selectedIndex = indexPath.row
        collectionView.performBatchUpdates(nil)
    }
    
  2. Tell your cells to layout their subviews every time the collection view layout object changes their layout attributes (which will occur within the performBatchUpdates animation block):

    // ProblematicCollectionViewCell.swift
    
    override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)
        layoutIfNeeded()
    }
    

If you want greater control over your animations, you can nest the call to performBatchUpdates inside a call to one of the UIView.animate block-based animation methods. The default animation duration for collection view cells in iOS 10 is 0.25.

like image 136
jamesk Avatar answered Nov 09 '22 23:11

jamesk


The solution is very easy. First, in ViewController.collectionView(_,didSelectItemAt:), write only this:

collectionView.performBatchUpdates({
        self.selectedIndex = indexPath.row
    }, completion: nil)

And then, in the class ProblematicCollectionViewCell add this func:

override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
    super.apply(layoutAttributes)

    self.layoutIfNeeded()
}

Enjoy!

like image 5
Gabriel Avatar answered Nov 10 '22 00:11

Gabriel


You can apply a transform to a cell, although it has some drawbacks, such as handling orientation changes.

For extra impact, I have added a color change and a spring effect in the mix, neither of which could be achieved using the reloading route:

    func collectionView(_ collectionView: UICollectionView,
                        didSelectItemAt indexPath: IndexPath) {
        UIView.animate(
            withDuration: 0.4,
            delay: 0,
            usingSpringWithDamping: 0.4,
            initialSpringVelocity: 0,
            options: UIViewAnimationOptions.beginFromCurrentState,
            animations: {
                if( self.selectedIndexPath.row != NSNotFound) {
                    if let c0 =
                        collectionView.cellForItem(at: self.selectedIndexPath)
                    {
                        c0.contentView.layer.transform = CATransform3DIdentity
                        c0.contentView.backgroundColor = UIColor.lightGray
                    }
                }
                self.selectedIndexPath = indexPath
                if let c1 = collectionView.cellForItem(at: indexPath)
                {
                    c1.contentView.layer.transform =
                        CATransform3DMakeScale(1.25, 1.25, 1)
                    c1.contentView.backgroundColor = UIColor.red
                }

            },
            completion: nil)
    }
like image 2
SwiftArchitect Avatar answered Nov 10 '22 00:11

SwiftArchitect