90

Note: The answer given here doesn't work for me.

I have a UIScrollView (not a table view, just a custom thing), and when the user takes certain actions, I want to kill any scrolling (dragging or deceleration) inside the view. I've tried doing e.g. this:

[scrollView scrollRectToVisible:CGRectInset([scrollView bounds], 10, 10) animated:NO];

on the theory that, given a rect that's already known visible, the scrolling will just stop where it is, but it turns out that this doesn't have any effect-- apparently the scroll view sees that the given rect is in bounds and takes no action. I can get the scroll to stop, if I give a rect that is definitely outside the currently-visible bounds, but inside the contentSize of the view. This seems to halt the view as expected... but also causes it to jump to some other location. I could probably do a little playing around at the margins to get this to work reasonably OK, but does anyone know of a clean way to halt a scroll view that's doing its thing?

Thanks.

Community
  • 1
  • 1
Ben Zotto
  • 70,108
  • 23
  • 141
  • 204

14 Answers14

112

I played with your original solution a bit, and this seems to work just fine. I think you almost had it, but you were just offsetting the rect that you used too much, and forgot that you could just scroll the rect straight back to the original rect.

The generalized solution for any scrolling action is this:

- (void)killScroll 
{
    CGPoint offset = scrollView.contentOffset;
    offset.x -= 1.0;
    offset.y -= 1.0;
    [scrollView setContentOffset:offset animated:NO];
    offset.x += 1.0;
    offset.y += 1.0;
    [scrollView setContentOffset:offset animated:NO];
}

[Edit] As of iOS 4.3 (and possibly earlier) this also appears to work

- (void)killScroll 
{
    CGPoint offset = scrollView.contentOffset;
    [scrollView setContentOffset:offset animated:NO];
}
nrj
  • 1,701
  • 2
  • 22
  • 37
David Liu
  • 9,426
  • 5
  • 40
  • 63
  • 1
    David: great, thanks. I had twiddled myself in this direction, but ended up with less trivial calculations about picking a 1x1 rect just outside the scroll bounds. Taking your suggestion of offsetting and then restoring immediately (which seems frankly to take advantage of an unpublished behavior where the successive calls in one event run actually work even though the "result" should be a no-op), it works fine. I'm going to edit your answer above to include the generalized solution that should work for any scroll direction. Thanks! – Ben Zotto Aug 06 '10 at 15:44
  • (Hope you don't mind the edits, wanted to clarify for later travelers. Thanks for your answer!) – Ben Zotto Aug 06 '10 at 15:51
  • looks great... by the way how do you get back to where you were? do you save the offset before killing it, and then get back there? – Lior Frenkel May 04 '11 at 14:13
  • the longer one worked fine for me, iOS6.1 on iPod that looks like iPhone 5 – braden Jun 05 '13 at 19:44
  • 2
    In iOS 7, when the scrollView isDecelerating, only the top solution works (the += 1, -= 1 solution). – Sahil Jul 25 '14 at 05:33
  • 1
    This solution can be improved. Use += 0.1 instead of += 1, so single call of `setContentOffset` will be enough. Scroll view will round up content offset automatically. – kelin Jun 01 '15 at 14:10
  • The `+=` bit seems to no longer be necessary under iOS 11 SDK with Xcode 9.2. Was able to get this working with swift using `contentTableView.setContentOffset(contentTableView.contentOffset, animated: false)` – Beltalowda Dec 15 '17 at 22:14
104

The generic answer is, that [scrollView setContentOffset:offset animated:NO] is not the same as [scrollView setContentOffset:offset] !

  • [scrollView setContentOffset:offset animated:NO] actually stops any running animation.
  • [scrollView setContentOffset:offset] doesn't stop any running animation.
  • Same for scrollView.contentOffset = offset: doesn't stop any running animation.

That's not documented anywhere, but that's the behavior as tested on iOS 6.1 & iOS 7.1 - probably also before.

So the solution to stop a running animation / deceleration is simple as that:

// Objective-C
[scrollView setContentOffset:scrollView.contentOffset animated:NO];
// Swift
scrollView.setContentOffset(scrollView.contentOffset, animated:false)

Basically what David Liu said in his edited answer. But I wanted to make clear, that these two APIs are NOT the same.

calimarkus
  • 9,955
  • 2
  • 28
  • 48
  • 13
    Well done on the explanation to the "reason seekers" among us... (: – Aviel Gross Jan 11 '15 at 12:49
  • 3
    This kinda makes me sad though. – DCMaxxx Nov 04 '15 at 13:44
  • 2
    I don't understand why this stuff is not documented...urggggghh – user1105951 Mar 21 '16 at 17:00
  • Even more interesting in (at least) iOS 11.3 and 11.4 is: If the contentOffset gets animated to e.g. (300, 0), then "contentOffset = (300, 0)" is assigned while the animation still runs, the contentOffset may reset to (0, 0)! This must be a bug! – Jan May 31 '18 at 09:24
74

For me, David Lui's accepted answer above didn't work for me. This is what I ended up doing:

- (void)killScroll {
    self.scrollView.scrollEnabled = NO;
    self.scrollView.scrollEnabled = YES;
}

For what it is worth, I'm using the iOS 6.0 iPhone Simulator.

Community
  • 1
  • 1
TPoschel
  • 3,775
  • 2
  • 30
  • 29
  • 2
    +1 like you said, the other didn't work for me either, but this solution worked great! – brenjt Jul 20 '13 at 16:28
  • 1
    Awesome, it works perfectly in `supportedInterfaceOrientations` to prevent the scroll events to continue if the user is **rotating the device while scrolling**, which is sometimes a mess depending on what you are doing. – cprcrack Oct 27 '13 at 21:30
21

This is what I do for my scroll views and all other related subclasses:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
 {
    *targetContentOffset = scrollView.contentOffset;
 }

This sets the targetContentOffset to the scrollView's current offset, thus making the scrolling to stop because it has reached the target. It actually makes sense to use a method whose purpose is that users could set the targeted contentOffset.

Swift

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    targetContentOffset.pointee = scrollView.contentOffset
}
Community
  • 1
  • 1
funct7
  • 3,407
  • 2
  • 27
  • 33
  • 1
    this looks like the one apple supposed developers to use. because all others lead to bugs at some point. i am surprised this is not the accepted answer. – OlDor Oct 13 '16 at 11:09
  • This is what this delegate method for. great answer. – Ryan Nov 30 '16 at 17:07
  • In my opinion this is the most elegant solution as Apple says in its documentation: "Your application can change the value of the targetContentOffset parameter to adjust where the scrollview finishes its scrolling animation.". Worked very well for me – Macistador May 09 '17 at 11:53
  • this is the right answer. None of the above worked for me – sethu Nov 03 '17 at 07:06
21

Stop the scroll in swift:

scrollView.setContentOffset(scrollView.contentOffset, animated: false)
budiDino
  • 13,044
  • 8
  • 95
  • 91
17

Actually ... The most "modern" way would be -->

scrollview.panGestureRecognizer.enabled = false;
scrollview.panGestureRecognizer.enabled = true;

This deactivates the gesture-recognizer that is responsible for scrolling for just a moment which will kill the current touch. The user would need to lift the finger and put it back down to start scrolling again.

Edit: This actually just kills the current dragging of the user but does not immediately stop the deceleration if the scrollview is in this state currently. To do this the accepted answers edit is pretty much the best way xD

[scrollview setContentOffset: scrollview.contentOffset animated:false];
Xatian
  • 772
  • 1
  • 8
  • 24
  • Well I use this in an App of mine and it does exactly what is asked by the OP. Can you please give a bit more context? What is not working or why? – Xatian Jul 27 '14 at 16:36
  • Oh sorry ... I think I know ... I just answered the part of the question that I was looking for ... the second part I overlooked ... :-) --> edited. – Xatian Jul 27 '14 at 16:42
  • 1
    Doesn't stop deceleration. – Rudolf Adamkovič Nov 05 '15 at 12:46
5

The cleanest way will be subclassing UIScrollView and providing your own setContentOffset method. This should pass the message on, only if you haven't switched on your freeze boolean property.

Like so:

BOOL freeze; // and the @property, @synthesize lines..

-(void)setContentOffset:(CGPoint)offset
{
    if ( !freeze ) [super setContentOffset:offset];
}

Then, to freeze:

scrollView.freeze = YES;
mvds
  • 45,755
  • 8
  • 102
  • 111
  • 1
    Thanks. While this halts the visible scrolling, it doesn't actually stop the internal deceleration. If you toggle this on, and then off again after a moment, you'll see the scrolling pause, and then jump ahead and continue to decelerate. So the scrollview's internal state is still scrolling. Thus you need to freeze until you know deceleration has stopped, which you can do in partnership with a delegate, but it feels a little goofy to me. I'm hoping for something that actual just zaps the deceleration immediately (e.g. has the effect of a single tap on the screen). – Ben Zotto Aug 05 '10 at 00:28
  • ok, these kind of issues sound familiar from prior experience... why not zap the deceleration by setting `decelerationRate = 1e10;` while freeze == YES? – mvds Aug 05 '10 at 00:41
  • (not knowing the internal math, 10*UIScrollViewDecelerationRateFast may be a wiser choice than 1e10) – mvds Aug 05 '10 at 00:43
  • Interesting idea! Unfortunately this value seems to be clamped. Tried it out, and there doesn't seem to be a value of this which will cause deceleration to immediately stop. Best I can do is make it sluggish. :) – Ben Zotto Aug 05 '10 at 02:15
  • plan B is then to disable scrolling for a brief period - maybe you can get away with it, since you're blocking setContentOffset anyway, hopefully preventing side-effects from disabling scrolling. – mvds Aug 05 '10 at 02:21
  • Apparently the deceleration works multiplicatively. If you print out the deceleration rates they define, it gives `fast = 0.99`, and `normal = 0.998`. Even so though, apparently setting deceleration to 0 doesn't work, sadly. – David Liu Aug 06 '10 at 05:40
4

This answer worked for me: Deactivate UIScrollView decelerating

-(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{
    [scrollView setContentOffset:scrollView.contentOffset animated:YES];
}
Community
  • 1
  • 1
Johnny Rockex
  • 4,136
  • 3
  • 35
  • 55
3

Disable just scroll user interaction. (swift)

scrollView.isScrollEnabled = false

Disable in during scroll animation after dragging. (swift)

var scrollingByVelocity = false

func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
    if !scrollingByVelocity {
        scrollView.setContentOffset(scrollView.contentOffset, animated: false)
    }
}
Brownsoo Han
  • 4,549
  • 3
  • 20
  • 20
2

SWift 5+

by using Extension

extension UIScrollView  {
    
    func stopDecelerating() {
        let contentOffset = self.contentOffset
        self.setContentOffset(contentOffset, animated: false)
    }
}

use

    myScrollView.stopDecelerating()
    // your stuff
Lakhdeep Singh
  • 1,212
  • 13
  • 9
1

This works for me in Swift 4.2:

   func killScroll() {
    self.scrollView.isScrollEnabled = false;
    self.scrollView.isScrollEnabled = true;
}

... as an extension:

extension UIScrollView {
    func killScroll() {
        self.isScrollEnabled = false;
        self.isScrollEnabled = true;

    }
}
StefanLdhl
  • 796
  • 7
  • 11
0

I have tried this methods in collectionview:

self.collectionView.collectionViewLayout.finalizeCollectionViewUpdates()

Kalerfu
  • 1
  • 1
0

I wanted to disable scrolling only when a certain UIView within the scrollview is the source of the touch during the swipe. It would have required quite a bit of refactoring to move the UIView outside of the UIScrollView, as we had a complex view hierarchy.

As a workaround, I added a single UIPanGestureRecognizer to the subview in which I wanted to prevent from scrolling. This UIPanGestureRecognizer will cancelsTouchesInView which prevents the UIScrollView's panGesture from activating.

It's a little bit of a 'hack', but it's a super easy change, and if you're using a XIB or Storyboard, all you need to do is drag the pan gesture onto the subview in question.

Daniel Williams
  • 256
  • 1
  • 12
0

Swift 5, Xcode 13

In my example, I have the situation where the user has a scroll view and image inside, and when the user begins zooming the image need prevent free-scrolling inside, I use the delegate function of the scroll view and content offset. Don't forget scrollView.delegate = self, if need full code don't shy notify my My code:

private var contentOffsetHolder: CGPoint = CGPoint(x: 0, y: 0)
    
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        contentOffsetHolder = scrollView.contentOffset
        
    }
    
    func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
        scrollView.setContentOffset(contentOffsetHolder, animated:false)
    }
Ice
  • 680
  • 1
  • 10
  • 23