Search code examples
iosuiscrollviewuiscrollviewdelegate

UIScrollViewDelegate scrollViewDidScroll method not keeping up with high-velocity scrolling (flick)?


I have a UIScrollView with a UIView header that sticks to the top of the scroll view and shrinks from full size to small size as the user scrolls downward. These are achieved, of course, through the scroll view delegate:

// scroll view delegate
extension SomeViewController {

    // scroll view did scroll
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let contentOffsetY = scrollView.contentOffset.y

        // make banner sticky
        banner.frame.origin.y = max(0, contentOffsetY)

        // shrink banner
        if contentOffsetY > 0 && contentOffsetY <= bannerHeight! - 64 {
            banner.frame.size.height = bannerHeight! - contentOffsetY
            bannerGraphic.frame = banner.bounds
        }

    }

}

It works great... if the user doesn't scroll with high velocity. If the user flicks to scroll downward (high velocity), the delegate doesn't seem to keep up and the banner never shrinks down completely (maybe 85-90% of the way). To verify this, I printed the offset of the scroll view to the console and noticed that when the user scrolls slowly, the console may print 100 lines of the current offset. And when the user scrolls fast, the console may print 25 lines of the current offset. The delegate simply cannot keep up with a high-velocity scroll.

Is there a way to make sure the banner shrinks regardless of scroll speed or is this just the way things are with UIKit?


Solution

  • The ScrollView Delegate does not receive a notification for every single point that the view scrolls. And you really wouldn't want that... it would be way too much unnecessary processing.

    So, it's entirely possible for large gaps to go by when scrolling very quickly. In fact, in a quick test I just did the .contentOffsetY value jumped from 0 to 75 to 300...

    Your code was close. What you want to do is handle the condition when you are past your maximum value.

    Give this a try (I think it will just drop-in and replace your function):

    extension TestViewController: UIScrollViewDelegate {
    
        // scroll view did scroll
        public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    
            let contentOffsetY = scrollView.contentOffset.y
    
            // make banner sticky
            banner.frame.origin.y = max(0, contentOffsetY)
    
            // if scrollView content is all the way at the top 
            // (or pulled down, waiting to "bounce back up")
    
            if contentOffsetY <= 0 {
    
                // set banner to original height
                banner.frame.size.height = bannerHeight
                bannerGraphic.frame = banner.bounds
    
            } else {
    
                // view has scrolled (user has dragged *up*)
    
                // set banner height to originalHeight - y offset,
                // but keep it a minimum of 64
    
                banner.frame.size.height = max(bannerHeight! - contentOffsetY, 64)
                bannerGraphic.frame = banner.bounds
    
            }
    
        }
    
    }