Detecting when UIScrollView is bouncing

Let’s say you want to have your UI change when a scroll view is bouncing (or over-scrolling).

First you need to conform to the UIScrollViewDelegate methods, more specifically scrollViewDidScroll(_ scrollView: UIScrollView).

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // TODO
    }
}

For example, let’s change the background color of the scroll view depending on bouncing status. If user is over-scrolling from the top, let’s change it to red, green if over-scrolling from the bottom, and white otherwise.

First, let’s detect when bouncing from the top:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let topInsetForBouncing = scrollView.safeAreaInsets.top != 0.0 ? -scrollView.safeAreaInsets.top : 0.0
    let isBouncingTop: Bool = scrollView.contentOffset.y < topInsetForBouncing - scrollView.contentInset.top

    if isBouncingTop {
        scrollView.backgroundColor = .red
    } else {
        scrollView.backgroundColor = .white
    }
}

It’s fairly easy, if the current vertical contentOffset is smaller than all space at the top, then we are over-scrolling. Because the top space will differ depending on devices, we have to use the safeAreaInsets, but mind that -0.0 is different than 0, thus the ternary operator.

Now let’s detect when the scroll view is bouncing from the bottom:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let topInsetForBouncing = scrollView.safeAreaInsets.top != 0.0 ? -scrollView.safeAreaInsets.top : 0.0
    let isBouncingTop: Bool = scrollView.contentOffset.y < topInsetForBouncing - scrollView.contentInset.top

    let bottomInsetForBouncing = scrollView.safeAreaInsets.bottom
    let threshold: CGFloat
    if scrollView.contentSize.height > scrollView.frame.size.height {
        threshold = (scrollView.contentSize.height - scrollView.frame.size.height + scrollView.contentInset.bottom + bottomInsetForBouncing)
    } else {
        threshold = topInsetForBouncing
    }
    let isBouncingBottom: Bool = scrollView.contentOffset.y > threshold

    if isBouncingTop {
        scrollView.backgroundColor = .red
    } else if isBouncingBottom {
        scrollView.backgroundColor = .green
    } else {
        scrollView.backgroundColor = .white
    }
}

It’s a bit more tricky in this case, scroll views are bouncing wether or not there is enough content for them to reach them, so we need to define what threshold the content offset needs to pass. When the content size is not big enough, then any movement that is not bouncing from the top is bouncing from the bottom. If the content size is big enough, then we can use a similar logic than before.

Let’s cleanup a little, this looks like a good candidate for an extension:

extension UIScrollView {
var isBouncing: Bool {
return isBouncingTop || isBouncingBottom
}
var isBouncingTop: Bool {
return contentOffset.y < topInsetForBouncing – contentInset.top
}
var isBouncingBottom: Bool {
let threshold: CGFloat
if contentSize.height > frame.size.height {
threshold = (contentSize.height – frame.size.height + contentInset.bottom + bottomInsetForBouncing)
} else {
threshold = topInsetForBouncing
}
return contentOffset.y > threshold
}
private var topInsetForBouncing: CGFloat {
return safeAreaInsets.top != 0.0 ? -safeAreaInsets.top : 0.0
}
private var bottomInsetForBouncing: CGFloat {
return safeAreaInsets.bottom
}
}

Now we simply need to call isBouncingTop or isBouncingBottom on the scroll view:

extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView.isBouncingTop {
            scrollView.backgroundColor = .red
        } else if scrollView.isBouncingBottom {
            scrollView.backgroundColor = .green
        } else {
            scrollView.backgroundColor = .white
        }
    }
}

And because UITableView and UICollectionView inherit from UIScrollView, it will work for these as well.


Leave a Reply

Your email address will not be published. Required fields are marked *