UIKit Dynamics in Collection View always ends in chaos

202 Views Asked by At

When scrolling with my "springy flow layout" (meant to replicate the scrolling effect in Messages), the initial scroll works well, but repeated scrolling ends with all of the cells constantly bouncing all over the screen, and off the vertical axis.

I don't understand why the cells are moving off the vertical axis, since there is no movement applied in the horizontal axis. I'm simply applying this flow layout to my collection view which is currently only setup to create a bunch of dummy cells.

How do I prevent movement in the horizontal axis and how do I ensure that the cells always come to a rest eventually

class SpringyColumnLayout: UICollectionViewFlowLayout {
    private lazy var dynamicAnimator = UIDynamicAnimator(collectionViewLayout: self)

    override func prepare() {
        super.prepare()

        guard let cv = collectionView else { return }

        let availableWidth = cv.bounds.inset(by: cv.layoutMargins).width

        let minColumnWidth: CGFloat = 300
        let maxNumberOfColumns = Int(availableWidth / minColumnWidth)
        let cellWidth = (availableWidth / CGFloat(maxNumberOfColumns))
            .rounded(.down)

        self.itemSize = CGSize(width: cellWidth, height: 70)

        self.sectionInset = UIEdgeInsets(
            top: minimumInteritemSpacing,
            left: 0,
            bottom: 0,
            right: 0
        )

        self.sectionInsetReference = .fromSafeArea

        if dynamicAnimator.behaviors.isEmpty {
            let contentSize = collectionViewContentSize
            let contentBounds = CGRect(origin: .zero, size: contentSize)
            guard let items = super.layoutAttributesForElements(in: contentBounds)
                else { return }

            for item in items {
                let spring = UIAttachmentBehavior(
                    item: item,
                    attachedToAnchor: item.center
                )
                spring.length = 0
                spring.damping = 0.8
                spring.frequency = 1

                self.dynamicAnimator.addBehavior(spring)
            }
        }
    }

    override func layoutAttributesForElements(
        in rect: CGRect
    ) -> [UICollectionViewLayoutAttributes]? {
        return dynamicAnimator.items(in: rect) as? [UICollectionViewLayoutAttributes]
    }

    override func layoutAttributesForItem(
        at indexPath: IndexPath
    ) -> UICollectionViewLayoutAttributes? {
        return dynamicAnimator.layoutAttributesForCell(at: indexPath)
    }

    override func shouldInvalidateLayout(
        forBoundsChange newBounds: CGRect
    ) -> Bool {
        let scrollView = self.collectionView!

        let scrollDelta = newBounds.origin.y - scrollView.bounds.origin.y
        let touchLocation = scrollView.panGestureRecognizer
            .location(in: scrollView)

        for case let spring as UIAttachmentBehavior in dynamicAnimator.behaviors {
            let anchorPoint = spring.anchorPoint
            let yDistanceFromTouch = abs(touchLocation.y - anchorPoint.y)
            let xDistanceFromTouch = abs(touchLocation.x - anchorPoint.x)
            let scrollResistance = (yDistanceFromTouch + xDistanceFromTouch) / 1500

            let item = spring.items.first!
            var center = item.center
            if scrollDelta < 0 {
                center.y += max(scrollDelta, scrollDelta * scrollResistance)
            } else {
                center.y += min(scrollDelta, scrollDelta * scrollResistance)
            }
            item.center = center

            dynamicAnimator.updateItem(usingCurrentState: item)
        }

        return false
    }
}
0

There are 0 best solutions below