Search code examples
swiftuicollectionviewlayout

UICollectionView: Resizing cells with animations and custom layouts


I have a collection view with a custom layout and a diffable datasource. I would like to resize the cells with an animation.

With a custom layout: it looks like the content of the cells scaled to the new size during the animation. Only when the cells reaches its final size, it is finally redrawn (see animation below)

With a stock flow layout the subviews are laid out correctly during the animation: the label remains at the top, no resizing and same for the switch.

So I would assume that the issue is in the layout implementation, what could be added to the layout allow a correct layout during the animation?

Description

The custom layout used in this example is FSQCollectionViewAlignedLayout, I also got it with the custom I used in my code. I noticed that for some cases, it is important to always return the same layout attributes objects, which FSQCollectionViewAlignedLayout doesn't do. I modified it to not return copies, and the result is the same.

For the cells (defined in the xib), the contentMode of the cell and its contentView are set to .top. The cell's contentView contains 2 subviews: a label (laid out in layoutSubviews) and a switch (laid out with constraints, but its presence has no effect on the animation).

The code of the controller is:

class ViewController: UIViewController, UICollectionViewDelegateFlowLayout {
    enum Section {
        case main
    }
    @IBOutlet weak var collectionView : UICollectionView!
    var layout = FSQCollectionViewAlignedLayout()
    var dataSource : UICollectionViewDiffableDataSource<Section, String>!
    var cellHeight : CGFloat = 100

    override func loadView() {
        super.loadView()
        
        layout.defaultCellSize = CGSize(width: 150, height: 100)
        collectionView.setCollectionViewLayout(layout, animated: false) // uncommented to use the stock flow layout
        
        dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { cv, indexPath, item in
            guard let cell = cv.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell else { return nil }
            cell.label.text = item
            return cell
        })
        
        var snapshot = NSDiffableDataSourceSnapshot<Section, String>()
        snapshot.appendSections([.main])
        snapshot.appendItems(["1", "2"], toSection: .main)
        dataSource.apply(snapshot)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 150, height: cellHeight)
    }
    
    @IBAction func resizeAction(_ sender: Any) {
        if let layout = collectionView.collectionViewLayout as? FSQCollectionViewAlignedLayout {
            UIView.animate(withDuration: 0.25) {
                layout.defaultCellSize.height += 50
                let invalidation = FSQCollectionViewAlignedLayoutInvalidationContext()
                invalidation.invalidateItems(at: [[0, 0], [0, 1]])
                layout.invalidateLayout(with: invalidation)
                self.view.layoutIfNeeded()
            }
        }
        if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
            UIView.animate(withDuration: 0.25) {
                self.cellHeight += 50
                let invalidation = UICollectionViewFlowLayoutInvalidationContext()
                invalidation.invalidateItems(at: [[0, 0], [0, 1]])
                layout.invalidateLayout(with: invalidation)
                self.view.layoutIfNeeded()
            }
        }
    }
}

class Cell : UICollectionViewCell {
    @IBOutlet weak var label: UILabel!
    
    override func layoutSubviews() {
        super.layoutSubviews()
        label.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 30)
    }
}

When resizing the content of the cell is scaled


Solution

  • I spent a developer support ticket on this question. The answer was that custom animations when reloading cells are not supported.

    So as a workaround, I ended up creating new cells instances that I add to the view hierarchy, then add autolayout constraints so that they overlap exactly the cells being resized. The animation is then run on those newly added cells. At the end of the animation, these cells are removed.