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.

2
  • 1
    I have the same problem, I am using a UITextView in a UITableView, when the text view is resized, UITableView->scrollViewDidScroll is trigged ;-( Commented Jun 29, 2012 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. Commented Jun 29, 2012 at 13:54

6 Answers 6

114

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;
Sign up to request clarification or add additional context in comments.

3 Comments

why so leery my man? it is all the content offset is really, and can be animated using UIView animation blocks and stuff.
This still makes another call to scrollViewDidScroll, but it does not do so until after the current scrollViewDidScroll function ends.
@Mark -- I don't see this queuing a call to scrollViewDidScroll. Is there some situation this would happen in?
83

Try

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

Worked for me.

Comments

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

8 Comments

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

1 Comment

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

1 Comment

Not sure what's going on, but in Xcode 9 - building for iOS 11, even when setting bounds, scrollViewDidScroll appears to fire.
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]; });
    }
}

1 Comment

Interesting, while not my issue in this case I'll keep this in mind.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.