UICollectionView snap scrolling and pagination
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.