Oleksii Nezhyborets Oleksii Nezhyborets - 11 days ago 6
iOS Question

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: []
}

Answer

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) {
        print("didSelectItemAt: \(indexPath)")
    
        self.selectedIndex = indexPath.row
        collectionView.performBatchUpdates(nil, completion: nil)
    }
    
    func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
        print("didDeselectItemAt: \(indexPath)")
    
        // When selecting a new item, didDeselectItemAt is called before didSelectItemAt,
        // so it is necessary to remove the selected index (i.e. this item's row index)
        // in order for sizeForItemAt to return the deselected size for this item.
    
        if self.selectedIndex == indexPath.row {
            self.selectedIndex = NSNotFound
        }
    
        collectionView.performBatchUpdates(nil, completion: 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.