Paging UIScrollView in increments smaller than frame size

Viewed 41624

I have a scroll view that is the width of the screen but only about 70 pixels high. It contains many 50 x 50 icons (with space around them) that I want the user to be able to choose from. But I always want the scroll view to behave in a paged manner, always stopping with an icon in the exact center.

If the icons were the width of the screen this wouldn't be a problem because the UIScrollView's paging would take care of it. But because my little icons are much less than the content size, it doesn't work.

I've seen this behavior before in an app call AllRecipes. I just don't know how to do it.

How do I get paging on a per-icon sized basis to work?

15 Answers

Refined Swift version of the UICollectionView solution:

  • Limits to one page per swipe
  • Ensures fast snap to page, even if your scroll was slow
override func viewDidLoad() {
    super.viewDidLoad()
    collectionView.decelerationRate = .fast
}

private var dragStartPage: CGPoint = .zero

func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    dragStartOffset = scrollView.contentOffset
}

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    // Snap target offset to current or adjacent page
    let currentIndex = pageIndexForContentOffset(dragStartOffset)
    var targetIndex = pageIndexForContentOffset(targetContentOffset.pointee)
    if targetIndex != currentIndex {
        targetIndex = currentIndex + (targetIndex - currentIndex).signum()
    } else if abs(velocity.x) > 0.25 {
        targetIndex = currentIndex + (velocity.x > 0 ? 1 : 0)
    }
    // Constrain to valid indices
    if targetIndex < 0 { targetIndex = 0 }
    if targetIndex >= items.count { targetIndex = max(items.count-1, 0) }
    // Set new target offset
    targetContentOffset.pointee.x = contentOffsetForCardIndex(targetIndex)
}

Old thread, but worth mentioning my take on this:

import Foundation
import UIKit

class PaginatedCardScrollView: UIScrollView {

    convenience init() {
        self.init(frame: CGRect.zero)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        _setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        _setup()
    }

    private func _setup() {
        isPagingEnabled = true
        isScrollEnabled = true
        clipsToBounds = false
        showsHorizontalScrollIndicator = false
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        // Asume the scrollview extends uses the entire width of the screen
        return point.y >= frame.origin.y && point.y <= frame.origin.y + frame.size.height
    }
}

This way you can a) use the entire width of the scrollview to pan / swipe and b) be able to interact with the elements that are out of the scrollview's original bounds

For the UICollectionView issue (which for me was a UITableViewCell of a collection of horizontally scrolling cards with "tickers" of the upcoming / prior card), I just had to give up on using Apple's native paging. Damien's github solution worked awesomely for me. You can tweak the tickler size by upping the header width and dynamically sizing it to zero when at the first index so you don't end up with a large blank margin

Related