Setting contentOffset programmatically triggers scrollViewDidScroll
Asked Answered
M

6

69

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.

Mcclimans answered 23/2, 2012 at 17:45 Comment(2)
I have the same problem, I am using a UITextView in a UITableView, when the text view is resized, UITableView->scrollViewDidScroll is trigged ;-(Bumgardner
Tarc's answer worked perfectly. Since then I hae come to understand modifying bounds property and gained an understanding of what is occurring.Mcclimans
S
113

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;
Sielen answered 23/2, 2012 at 18:5 Comment(3)
why so leery my man? it is all the content offset is really, and can be animated using UIView animation blocks and stuff.Sielen
This still makes another call to scrollViewDidScroll, but it does not do so until after the current scrollViewDidScroll function ends.Aetiology
@Mark -- I don't see this queuing a call to scrollViewDidScroll. Is there some situation this would happen in?Shorn
J
84

Try

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

Worked for me.

Jezabel answered 25/7, 2013 at 11:10 Comment(0)
B
63

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.
    }
}
Banner answered 8/11, 2014 at 16:35 Comment(8)
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.Mcclimans
@Mcclimans - 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.Spent
@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.Mcclimans
@Mcclimans - 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.Spent
it's the only solution who can handle animated offset as well, all the other solution disable animation.Rabies
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.Sisterhood
This ans works with tableView triggering scrollViewDidScroll problem as wellZaragoza
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.Joule
C
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).
Caylacaylor answered 11/9, 2014 at 1:39 Comment(1)
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.Spireme
S
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
Shorn answered 10/3, 2015 at 12:33 Comment(1)
Not sure what's going on, but in Xcode 9 - building for iOS 11, even when setting bounds, scrollViewDidScroll appears to fire.Proudman
T
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]; });
    }
}
Truong answered 31/3, 2012 at 21:0 Comment(1)
Interesting, while not my issue in this case I'll keep this in mind.Mcclimans

© 2022 - 2024 — McMap. All rights reserved.