Setting contentOffset programmatically triggers scrollViewDidScroll

38,225

Solution 1

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;

Solution 2

Try

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

Worked for me.

Solution 3

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.
    }
}

Solution 4

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).

Solution 5

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
Share:
38,225
DBD
Author by

DBD

I code therefore I am.

Updated on July 08, 2022

Comments

  • DBD
    DBD almost 2 years

    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.

  • Tark
    Tark about 12 years
    why so leery my man? it is all the content offset is really, and can be animated using UIView animation blocks and stuff.
  • DBD
    DBD about 12 years
    Interesting, while not my issue in this case I'll keep this in mind.
  • Admin
    Admin about 12 years
    This still makes another call to scrollViewDidScroll, but it does not do so until after the current scrollViewDidScroll function ends.
  • DBD
    DBD over 9 years
    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.
  • SimplGy
    SimplGy about 9 years
    @Mark -- I don't see this queuing a call to scrollViewDidScroll. Is there some situation this would happen in?
  • Shaheen Ghiassy
    Shaheen Ghiassy over 8 years
    @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.
  • DBD
    DBD over 8 years
    @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.
  • Shaheen Ghiassy
    Shaheen Ghiassy over 8 years
    @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.
  • Blacky
    Blacky over 8 years
    it's the only solution who can handle animated offset as well, all the other solution disable animation.
  • Marchy
    Marchy over 7 years
    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.
  • brandonscript
    brandonscript over 5 years
    Not sure what's going on, but in Xcode 9 - building for iOS 11, even when setting bounds, scrollViewDidScroll appears to fire.
  • Chris Chute
    Chris Chute almost 4 years
    Best answer here. One modification though: I'd use scrollViewDidEndDragging(_:willDecelerate:) 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.
  • hbtpoprock
    hbtpoprock almost 3 years
    This ans works with tableView triggering scrollViewDidScroll problem as well
  • aheze
    aheze over 2 years
    This works perfectly, thanks!