Photo by rawpixel on Unsplash

UICollectionView snap scrolling and pagination

Roland Leth
2 min readAug 14, 2016

This is a snapping logic for a collection view with cells of same size, and one section (logic for more sections can be easily added).

scrollViewWillEndDragging has an inout targetContentOffset parameter, meaning we can read and modify the end position of the scroll. Luckily, we don’t need to take into consideration insets, line or item spacing (lost a lot of time by including them, then not being able to understand why the correct math produces wrong results), but we do need to consider the case where the user scrolls past the last page — the targetContentOffset will be within bounds, but the current contentOffset won’t be, so we need to check for that as well:

// Get our cell width 
let cellWidth = collectionView(
collectionView,
layout: collectionView.collectionViewLayout,
sizeForItemAtIndexPath: NSIndexPath(forItem: 0, inSection: 0) ).width
let page: CGFloat // Calculate the proposed "page"
let proposedPage = targetContentOffset.memory.x / cellWidth
// 3.25 should return page 3: floor(3.95) == floor(3)
// 3.3+ should return page 4: floor(4.0+) != floor(3)
if floor(proposedPage + 0.7) == floor(proposedPage)
&& scrollView.contentOffset.x <= targetContentOffset.memory.x {
page = floor(proposedPage)
}
else {
page = floor(proposedPage + 1)
}
// Replace the end position of the scroll
targetContentOffset.memory = CGPoint(
x: cellWidth * page,
y: targetContentOffset.memory.y
)

If “true” pagination is desired, as in scroll one page at a time, we need to change a little bit:

// We need to save the starting point 
private var startingScrollingOffset = CGPoint.zero
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
startingScrollingOffset = scrollView.contentOffset
}
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// [...]
// First, we use the current contentOffset
// instead of the target one
let proposedPage = scrollView.contentOffset.x / cellWidth

// If we scroll forward, we need to pass 10% of a page:
// floor(3.1 + 0.9) != floor(3)

// If we scroll backwards, we need to reach below 90%
// of the previous one: floor(2.89 + 0.1) == floor(2)

let delta: CGFloat = scrollView.contentOffset.x
> startingScrollingOffset.x ? 0.9 : 0.1
// Then, instead of using a flat value, we use the delta value,
// and we also remove the targetContentOffset logic
if floor(proposedPage + delta) == floor(proposedPage) {
// [...]
}

While the percentages were randomly picked, 0.1 feels a bit better for true pagination, while 0.3 feels better for snapped scrolling.

You can find more articles like this on my blog, or you can subscribe to my monthly newsletter. Originally published at rolandleth.com.

--

--

Roland Leth
Roland Leth

Written by Roland Leth

iOS & web developer. Blogger about life and tech at https://rolandleth.com. Founder at https://runtimesharks.com.

Responses (1)