---
title: "UICollectionView snap scrolling and pagination"
slug: uicollectionview-snap-scrolling-and-pagination
section: tech
date: 2016-05-20T13:02:00.000Z
canonical: https://roland.leth.ro/blog/tech/uicollectionview-snap-scrolling-and-pagination
---

The following snapping logic is for a collection with cells of the same size and one section, but the logic for more sections shouldn't be much different, or much more complex.

`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 (*I've lost a lot of time by including them, then not being able to understand why the correct math produces wrong results*):

```swift
let cellWidth = collectionView( // 1
  collectionView,
  layout: collectionView.collectionViewLayout,
  sizeForItemAt: IndexPath(item: 0, section: 0)
).width

let page: CGFloat
let snapPoint: CGFloat = 0.3
let snapDelta: CGFloat = 1 - snapPoint
let proposedPage = targetContentOffset.pointee.x / max(1, cellWidth) // 2

if floor(proposedPage + snapDelta) == floor(proposedPage)
  && scrollView.contentOffset.x <= targetContentOffset.pointee.x { // 3
  page = floor(proposedPage) // 4
}
else {
  page = floor(proposedPage + 1) // 5
}

targetContentOffset.pointee = CGPoint( // 6
  x: cellWidth * page,
  y: targetContentOffset.pointee.y
)
```

First, we'll need our cell width (1) so we can calculate the "proposed page" (2): this is the "page" we're at during scrolling (for example `3.25` would mean page `3` and a quarter of the fourth). If our desired `snapPoint` is `30%`, then our `snapDelta` would be `1 - 0.3 = 0.7`. Think of it like this:

* if we have reached/passed `30%` of a page, then we virtually reached its end and we need to scroll to the beginning of the next one: `3.3 + 0.7 = 4.0` and `floor(4.0) > floor(3.0)` (5)
* if we haven't reached `30%` of a page, then we need to stay on it by scrolling to its beginning: `3.25 + 0.7 = 3.95` and `floor(3.95) == floor(3.0)` (4)

We also need to consider the case where the user scrolls past the last page (3) - the `targetContentOffset` will be within bounds, but the current `contentOffset` won't be, so we need to check for that as well (5).

Finally, we replace the `targetContentOffset` with our computed value (6).

> Update, Feb 13, 2017: Based on the previous point of "insets don't have to be taken into account", I've lost a lot of time in a recent project, when I actually had to take them into consideration (for "true" pagination, at least), so I'm a bit lost about this.

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

```swift
private var startingScrollingOffset = CGPoint.zero
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  startingScrollingOffset = scrollView.contentOffset // 1
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  // [...]
  let offset = scrollView.contentOffset.x + scrollView.contentInset.left // 2
  let proposedPage = offset / max(1, cellWidth)
  let snapPoint: CGFloat = 0.1
  let snapDelta: CGFloat = offset > startingScrollingOffset.x ? (1 - snapPoint) : snapPoint

  if floor(proposedPage + snapDelta) == floor(proposedPage) { // 3
    page = floor(proposedPage) // 4
  }
  else {
    page = floor(proposedPage + 1) // 5
  }

  targetContentOffset.pointee = CGPoint(
    x: cellWidth * page,
    y: targetContentOffset.pointee.y
  )
}
```

We'll now need to save the position when scrolling started (1). Then, we'll use the current `contentOffset` and left inset instead of the `targetContentOffset` (2). The `snapDelta` logic changes too, as follows, based on direction:

* swiping left:
  * if we have reached/passed `10%` of the current page, then we virtually reached its end and we need to scroll to the next one: `3.1 + 0.9 = 4.0` and `floor(4.0) > floor(3.0)` (5)
  * if we haven't reached `10%` of the current page, we need to stay on it, by scrolling back to our starting point: `3.05 + 0.9 = 3.95` and `floor(3.95) == floor(3.0)` (4)

* swiping right:
  * if we have passed `90%` of the previous page, then we virtually passed its end and we need to scroll to its beginning: `2.89 + 0.1 = 2.99` and `floor(2.99) < floor(3.0)` (5)
  * if we haven't reached `90%` of the previous page, we need to stay on the current page, by scrolling back to our starting point: `2.91 + 0.1 = 3.01` and `floor(3.01) == floor(3.0)` (4)

As for calculating our page: we'll remove the `targetContentOffset` logic from the condition and use the `snapDelta` we just computed (3) instead of a flat value of `0.7`.

While the percentages were randomly picked, `0.1` feels a bit better for true pagination, while `0.3` feels better for snapped scrolling. If you have any feedback, drop by to chat [@roland.leth.ro](https://bsky.app/profile/roland.leth.ro).
