Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView animating cell beyond bounds

I set up a UICollectionView that has a following settings:

  • collectionView fits screen bounds
  • only vertical scroll is applied
  • most of cells fit to content's width
  • some of cells can change their heights on user interaction dynamically (animated)

It's pretty much like a UITableView, which works fine in most cases, except one specific situation when the animation doesn't apply.

Among stacked cells in collectionView, say one of the upper cells expands its height. Then the lower cell must be moving downwards to keep the distance. If this moving cell's target frame is out of collectionView's bounds, then no animation applies and the cell disappears.

Opposite case works the same way; if the lower cell's source frame is out of screen bounds (currently outside of the bounds) and the upper cell should shrink, no animation applies and it just appear on target frame.

This seems appropriate in memory management logic controlled by UICollectionView, but at the same time nothing natural to show users that some of contents just appear or disappear out of blue. I had tested this with UITableView and the same thing happens.

Is there a workaround for this issue?

like image 431
Tack-Gyu Lee Avatar asked Nov 21 '16 02:11

Tack-Gyu Lee


1 Answers

You should add some code or at least a gif of your UI problem.

I tried to replicate your problem using a basic UICollectionViewLayout subclass :

protocol CollectionViewLayoutDelegate: AnyObject {
    func heightForItem(at indexPath: IndexPath) -> CGFloat
}

class CollectionViewLayout: UICollectionViewLayout {
    weak var delegate: CollectionViewLayoutDelegate?

    private var itemAttributes: [UICollectionViewLayoutAttributes] = []

    override func prepare() {
        super.prepare()
        itemAttributes = generateItemAttributes()
    }

    override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
        return collectionView?.contentOffset ?? .zero
        return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return itemAttributes.first { $0.indexPath == indexPath }
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return itemAttributes.filter { $0.frame.intersects(rect) }
    }

    private func generateItemAttributes() -> [UICollectionViewLayoutAttributes] {
        var offset: CGFloat = 0
        return (0..<numberOfItems()).map { index in
            let indexPath = IndexPath(item: index, section: 0)
            let frame = CGRect(
                x: 0,
                y: offset,
                width: collectionView?.bounds.width ?? 0,
                height: delegate?.heightForItem(at: indexPath) ?? 0
            )
            offset = frame.maxY
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = frame
            return attributes
        }
    }
}

In a simple UIViewController, I reloaded the first cell each time a cell is selected:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    updatedIndexPath = IndexPath(item: 0, section: 0)
    collectionView.reloadItems(at: [updatedIndexPath])
}

In that case, I faced an animation like this:

bug

How to fix it ?

I think you could try to tweak the attributes returned by super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath) computing its correct frame and play with the z-index.

But you could also simply try to invalidate all the layout like so:

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    layout = CollectionViewLayout()
    layout.delegate = self
    collectionView.setCollectionViewLayout(layout, animated: true)
}

and override:

override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
    return collectionView?.contentOffset ?? .zero
}

to avoid a wrong target content offset computation when the layout is invalidated.

fix

like image 143
GaétanZ Avatar answered Nov 03 '22 00:11

GaétanZ