Search code examples
iosscrolluicollectionviewuiscrollview

Scroll UICollectionView at a given speed using UIPanGestureRecognizer


I am trying to implement a similar photo selection method to the stock iOS photos app. This works using a UIPanGestureRecognizer which disables the scroll on the UICollectionView. In the stock photos app, it seems like when the pan reaches the top or bottom of the screen the scroll view starts scrolling at a given speed and the closer to the edge of the screen, the quicker the scroll.

It doesn't seem as if there's an API to scroll at a given speed in a UIScrollView or UICollectionView. Is there any clever way I could do this with the existing methods such as scrollToVisibleRect or setting the content offset within an animation block? I'm worried that whilst these may work, they will be jerky in their movement!


Solution

  • I ended up creating a SpeedScrollableCollectionViewController for this and controlling it using a UIPanGestureRecognizer (not relevant to the question

    class SpeedScrollableCollectionViewController: UICollectionViewController {
    
        private var lastFrameTime: CFTimeInterval?
        
        private var displayLink: CADisplayLink?
       
        override init(collectionViewLayout layout: UICollectionViewLayout) {
            super.init(collectionViewLayout: layout)
        }
        
        override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
            super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        }
       
        required init?(coder: NSCoder) {
            super.init(coder: coder)
        }
        
        var scrollSpeed: CGPoint = .zero {
            didSet {
                guard scrollSpeed != .zero else {
                    displayLink?.isPaused = true
                    return
                }
                guard displayLink == nil else {
                    lastFrameTime = CACurrentMediaTime()
                    displayLink?.isPaused = false
                    return
                }
                lastFrameTime = CACurrentMediaTime()
                displayLink = CADisplayLink(target: self, selector: #selector(updateContentOfffset))
                displayLink?.add(to: .main, forMode: .common)
            }
        }
        
        @objc func updateContentOfffset() {
            
            defer {
                lastFrameTime = CACurrentMediaTime()
            }
            
            guard let lastFrameTime = lastFrameTime else { return }
            
            let dt = CACurrentMediaTime() - lastFrameTime
            let dx = scrollSpeed.x * CGFloat(dt)
            let dy = scrollSpeed.y * CGFloat(dt)
            
            var currentOffset = collectionView.contentOffset
            currentOffset.x += dx
            currentOffset.y += dy
            collectionView.setContentOffset(currentOffset, animated: false)
        }
    }