9

I am trying to implement a scroll view that snaps to points while scrolling.

All the posts here I've seen about snapping to a point 'after' the user has ended dragging the scroll. I want to make it snap during dragging.

So far I have this to stop the inertia after dragging and it works fine:

func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
      targetContentOffset.memory = scrollView.contentOffset
}

I tried this but not working as desired:

    var scrollSnapHeight : CGFloat = myScrollView.contentSize.height/10

scrollViewDidScroll:

func scrollViewDidScroll(scrollView: UIScrollView) {
    let remainder : CGFloat = scrollView.contentOffset.y % scrollSnapHeight
    var scrollPoint : CGPoint = scrollView.contentOffset
    if remainder != 0 && scrollView.dragging
    {
        if self.lastOffset > scrollView.contentOffset.y //Scrolling Down
        {
            scrollPoint.y += (scrollSnapHeight - remainder)
            NSLog("scrollDown")
        }
        else //Scrolling Up
        {
            scrollPoint.y -= (scrollSnapHeight - remainder)
        }
        scrollView .setContentOffset(scrollPoint, animated: true)
    }
    self.lastOffset = scrollView.contentOffset.y;
}
Gizmodo
  • 3,151
  • 7
  • 45
  • 92

2 Answers2

7

This approach is going to enable / disable scrollEnabled property of UIScrollView.

When scrollView scrolls outside the given scrollSnapHeight, make scrollEnabled to false. That will stop the scrolling. Then make scrolling enable again for the next drag.

extension ViewController: UIScrollViewDelegate {

    func scrollViewDidScroll(scrollView: UIScrollView) {

        if scrollView.contentOffset.y > lastOffset + scrollSnapHeight {
            scrollView.scrollEnabled = false
        } else if scrollView.contentOffset.y < lastOffset - scrollSnapHeight {
            scrollView.scrollEnabled = false
        }
    }

    func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {

        guard !decelerate else {
            return
        }

        setContentOffset(scrollView)
    }

    func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {

        setContentOffset(scrollView)
    }
}

func setContentOffset(scrollView: UIScrollView) {

    let stopOver = scrollSnapHeight
    var y = round(scrollView.contentOffset.y / stopOver) * stopOver
    y = max(0, min(y, scrollView.contentSize.height - scrollView.frame.height))
    lastOffset = y

    scrollView.setContentOffset(CGPointMake(scrollView.contentOffset.x, y), animated: true)

    scrollView.scrollEnabled = true
}
Warif Akhand Rishi
  • 23,920
  • 8
  • 80
  • 107
  • When I try this, after snapping, user has to lift the finger up, then start dragging again. Is there a way to make without lifting touch up? – Gizmodo Jul 19 '16 at 16:58
  • @gizmodo I couldn't understand. Snapping to position means users can drag but scrollview will not scroll. Now on which event you want to make the scrollView scroll again? – Warif Akhand Rishi Jul 20 '16 at 05:10
  • @gizmodo If you want to snap after user stops dragging scrollView, http://stackoverflow.com/a/35197919/1378447 – Warif Akhand Rishi Jul 20 '16 at 05:16
  • When I tried your sample code, it works the way I wanted except, after snapping, scroll view would stop scrolling. You have to lift the finger up and start scrolling again to resume. If you look at Apple's iOS 9 Photo App's Edit feature, you can see they snap and resume without having to lift. It snaps to default position. Looks like scrollView.scrollEnabled = false would make the panning end. – Gizmodo Jul 20 '16 at 12:56
  • @gizmodo sorry I couldn't find the snapping while dragging in the Photos app's edit feature. I think you are right about scroll disable ends the panning gesture. – Warif Akhand Rishi Jul 21 '16 at 15:45
  • if you open a photo in iOS9, Edit, then the small knob icon right next to filters icon, in the adjustments there... http://i.stack.imgur.com/1k57B.png – Gizmodo Jul 21 '16 at 16:04
  • @gizmodo got it :-). I'm not sure I can make it as smooth as apple does. but i'll try and let you know. – Warif Akhand Rishi Jul 22 '16 at 04:45
4

Subclass UIScrollView/UICollectionView

This solution does not require you lift your finger in order to unsnap and works while scrolling. If you need it for vertical scrolling and not horizontal scrolling just swap the x's with y's.

Set snapPoint to the content offset where you want the center of the snap to be.

Set snapOffset to the radius you want around the snapPoint for where snapping should occur.

If you need to know if the scrollView has snapped, just check the isSnapped variable.

class UIScrollViewSnapping : UIScrollView {
    public var snapPoint: CGPoint?
    public var snapOffset: CGFloat?
    public var isSnapped = false
    
    public override var contentOffset: CGPoint {
        set {
            if let snapPoint = self.snapPoint,
                let snapOffset = self.snapOffset,
                newValue.x > snapPoint.x - snapOffset,
                newValue.x < snapPoint.x + snapOffset {
                self.isSnapped = true
                super.contentOffset = snapPoint
            }
            else {
                self.isSnapped = false
                super.contentOffset = newValue
            }
        }
        get {
            return super.contentOffset
        }
    }
}
Community
  • 1
  • 1
Josh Bernfeld
  • 4,246
  • 2
  • 32
  • 35