68

I've got a a few UIScrollView on a page. You can scroll them independently or lock them together and scroll them as one. The problem occurs when they are locked.

I use UIScrollViewDelegate and scrollViewDidScroll: to track movement. I query the contentOffset of the UIScrollView which changed and then reflect change to other scroll views by setting their contentOffset property to match.

Great.... except I noticed a lot of extra calls. Programmatically changing the contentOffset of my scroll views triggers the delegate method scrollViewDidScroll: to be called. I've tried using setContentOffset:animated: instead, but I'm still getting the trigger on the delegate.

How can I modify my contentOffsets programmatically to not trigger scrollViewDidScroll:?

Implementation notes.... Each UIScrollView is part of a custom UIView which uses delegate pattern to call back to the presenting UIViewController subclass that handles coordinating the various contentOffset values.

DBD
  • 23,075
  • 12
  • 60
  • 84
  • 1
    I have the same problem, I am using a UITextView in a UITableView, when the text view is resized, UITableView->scrollViewDidScroll is trigged ;-( – Thomas Decaux Jun 29 '12 at 13:16
  • Tarc's answer worked perfectly. Since then I hae come to understand modifying bounds property and gained an understanding of what is occurring. – DBD Jun 29 '12 at 13:54

6 Answers6

112

It is possible to change the content offset of a UIScrollView without triggering the delegate callback scrollViewDidScroll:, by setting the bounds of the UIScrollView with the origin set to the desired content offset.

CGRect scrollBounds = scrollView.bounds;
scrollBounds.origin = desiredContentOffset;
scrollView.bounds = scrollBounds;
Tark
  • 5,153
  • 3
  • 24
  • 23
  • 2
    why so leery my man? it is all the content offset is really, and can be animated using UIView animation blocks and stuff. – Tark Feb 24 '12 at 15:27
  • 5
    This still makes another call to scrollViewDidScroll, but it does not do so until after the current scrollViewDidScroll function ends. –  Apr 21 '12 at 15:52
  • 1
    @Mark -- I don't see this queuing a call to `scrollViewDidScroll`. Is there some situation this would happen in? – SimplGy Mar 10 '15 at 12:14
83

Try

id scrollDelegate = scrollView.delegate;
scrollView.delegate = nil;
scrollView.contentOffset = point;
scrollView.delegate = scrollDelegate;

Worked for me.

Abhijit
  • 971
  • 6
  • 3
61

What about using existing properties of UIScrollView?

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
        /// The content offset was changed programmatically.
        /// Your code goes here.
    }
}
aheze
  • 24,434
  • 8
  • 68
  • 125
Kukosk
  • 2,892
  • 1
  • 27
  • 30
  • Possible, but fragile. If the `UIScrollView` API changed by even adding a new similar property it would cause your code to break. You would also have to worry about the flag these flags being gone. After all, I was checking in "did change" and the scroll view might no longer have the flag because the action is complete. – DBD Nov 10 '14 at 12:57
  • 11
    @DBD - If you worried about about every API changing in UIKit I don't know how you would get anything done. Relying on publicly documented APIs is not fragile. – Shaheen Ghiassy Sep 23 '15 at 16:11
  • @Shaheen - I'm not saying the solution is made of spun glass, but I found it to be less future proof than desired. The premise of the solution is checks 3 potential activities and if it's not those activities assume by process of elimination it is the 4th. My concern isn't about Apple just changing a public API, it's about Apple adding functionality to the API which would require a logic update in order for the code to continue functioning and Apple does make additions to the UIKit quite regularly, both as new classes and additions to existing classes. It's ok if you don't like my reasons. – DBD Sep 23 '15 at 16:35
  • @DBD - I can agree with you that "The premise of the solution is checks 3 potential activities and if it's not those activities assume by process of elimination it is the 4th"is less than desirable logic. – Shaheen Ghiassy Sep 23 '15 at 17:50
  • it's the only solution who can handle animated offset as well, all the other solution disable animation. – Blacky Jan 04 '16 at 13:14
  • 8
    Best answer on here by far. Unlike the bounds solution, it is not some mysterious side-effect that absolutely COULD change with future behaviour, and the likelihood of scroll-view having new concepts introduced is really low unless new touch interaction kinds get introduced – which let's face it, 10 years into iOS (we're even passed force touch now) is highly unlikely to happen. And if it does... big whoop, add one more interaction to your code and you're done. Could even spin it into an extension (with 'ieUserInteracting' property) and handles all your scroll views in one shot. Done. – Marchy Jan 09 '17 at 16:37
  • This ans works with tableView triggering scrollViewDidScroll problem as well – hbtpoprock May 25 '21 at 09:11
  • This should be the accepted answer. DBD did not like it because it was "fragile", but almost ten years later the API did not change. The accepted and most voted answer really looks like a hack, way more "fragile" than this one. – Martin May 09 '23 at 09:22
10

Another approach is to add some logic in your scrollViewDidScroll delegate to determine whether or not the change in content offset was triggered programatically or by the user's touch.

  • Add an 'isManualScroll' boolean variable to your class.
  • Set its initial value to false.
  • In scrollViewWillBeginDragging set it to true.
  • In your scrollViewDidScroll check to see that is it true and only respond if it is.
  • In scrollViewDidEndDecelerating set it to false.
  • In scrollViewWillEndDragging add logic to set it to false if the velocity is 0 (as scrollViewDidEndDecelerating won't be called in this case).
Jake MacMullin
  • 239
  • 3
  • 4
  • Best answer here. One modification though: I'd use [scrollViewDidEndDragging(_:willDecelerate:)](https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619436-scrollviewdidenddragging) instead of scrollViewWillEndDragging. Then instead of checking that the velocity is 0, you check that decelerate is false, which explicitly tells you that scrollViewWillEndDecelerating will not get called. – Chris Chute Jun 16 '20 at 16:48
6

Simplifying @Tark's answer, you can position the scrollview without firing scrollViewDidScroll in one line like this:

scrollView.bounds.origin = CGPoint(x:0, y:100); // whatever values you'd like
SimplGy
  • 20,079
  • 15
  • 107
  • 144
  • Not sure what's going on, but in Xcode 9 - building for iOS 11, even when setting bounds, `scrollViewDidScroll` appears to fire. – brandonscript Sep 20 '18 at 20:36
4

This is not a direct answer to the question, but if you are getting what appear to be spurious such messages, it can ALSO be because you are changing the bounds. I am using some Apple sample code with a "tilePages" method that removes and adds subview to a scrollview. This infrequently results in additional scrollViewDidScroll: messages called immediately, so you get into a recursion which you for sure didn't expect. In my case I got a nasty impossible to find crash.

What I ended up doing was queuing the call on the main queue:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if(scrollView == yourScrollView) {
        // dispatch fixes some recursive call to scrollViewDidScroll in tilePages (related to removeFromSuperView)
        // The reason can be found here: http://stackoverflow.com/questions/9418311
        dispatch_async(dispatch_get_main_queue(), ^{ [self tilePages]; });
    }
}
David H
  • 40,852
  • 12
  • 92
  • 138