Paging UIScrollView in increments smaller than frame size
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
UIScrollView's built-in isPagingEnabled snaps to multiples of the scroll view's frame size. To page in smaller increments (such as showing a card carousel where adjacent cards peek from the edges), you must disable isPagingEnabled and implement custom snapping in the scroll view delegate's scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) method. Alternatively, you can use a UICollectionView with UICollectionViewFlowLayout paging behavior or the modern UICollectionViewCompositionalLayout with orthogonal scrolling.
The Problem with Default Paging
With isPagingEnabled = true, the scroll view snaps in increments of 375 points (its frame width). There is no built-in property to change the paging increment.
Custom Paging with Delegate
Disable built-in paging and control snapping manually:
The key is scrollViewWillEndDragging(_:withVelocity:targetContentOffset:) — it lets you modify the target offset before the deceleration animation begins.
Clip-to-Bounds Trick for Peeking Cards
To show adjacent cards peeking from the edges while still using built-in paging:
The scroll view's frame is 295pt wide, so paging snaps in 295pt increments. Setting clipsToBounds = false makes adjacent cards visible beyond the frame edges.
UICollectionView with Custom Paging
UICollectionView provides more structured paging control:
Override targetContentOffset(forProposedContentOffset:withScrollingVelocity:) on the layout to control snapping behavior.
Common Pitfalls
- Leaving
isPagingEnabled = truewith custom snapping: IfisPagingEnabledis true, the scroll view's built-in paging overrides the delegate'stargetContentOffset. Always setisPagingEnabled = falsewhen implementing custom paging logic. - Forgetting
decelerationRate = .fast: Without fast deceleration, the scroll view glides too far after a flick, making custom snapping feel sluggish. Set.fastto match the feel of native paging. - Not handling velocity in the delegate: Without velocity checks, a fast flick may snap to the current page instead of advancing. Check
velocity.xto determine whether the user intended to advance or retreat. - Setting
clipsToBounds = falsewithout handling touches: WhenclipsToBounds = false, content is visible outside the scroll view's frame but taps on that content are not registered. OverridehitTest(_:with:)on the parent view to forward touches to the scroll view. - Incorrect
contentSizecalculation: IfcontentSizedoes not account for spacing between pages, the last page may not be reachable or may snap to the wrong position. Calculate as(pageWidth + spacing) * pageCount.
Summary
isPagingEnabledonly supports paging at the scroll view's frame width — no built-in way to change the increment- Implement custom paging by disabling
isPagingEnabledand usingscrollViewWillEndDragging(_:withVelocity:targetContentOffset:)to snap to custom increments - Use the
clipsToBounds = falsetrick to show peeking adjacent cards with built-in paging on a smaller frame - For collection views, override
targetContentOffset(forProposedContentOffset:withScrollingVelocity:)on a custom flow layout - Always set
decelerationRate = .fastand handle velocity for natural-feeling paging - Forward touches to the scroll view when using
clipsToBounds = falseto keep off-frame content interactive

