I don’t know about you, but it’s happened to me where I’ve needed to know when a scroll view is currently scrolling, and ONLY when it is scrolling. This means, when it is visually moving. You see, there are delegate methods such as scrollViewDidScroll:
but these get called if the contentOffset
changes. This doesn’t mean you have observed a visually moving scrollView.
I guess the most obvious use case is when you are hacking a scrollView to allow for “infinite scrolling”. There are many approaches to take when doing such work, but inevitably you’re most likely going to be trying to shift your subviews to different locations and then programmatically reset your contentOffset
to contentOffset + contentWidth
or some multiple of a constant that’s related to your content, or something like that.
With me so far? All I want is a scrollView that has the following
// KVO Observable @property (nonatomic, readonly, getter = isScrolling) BOOL scrolling;
(If you are unfamiliar with KVO, I would say Apple has the fundamental documentation, but if you’re a bit of a blacksmith like me, such documents, although incredibly exact, also put me to sleep. So, I recommend you just google “KVO Objective-C Tutorial” and see what you find.)
So here it is. The problem is, you need to use the scrollView delegate to make it work, but you don’t want to steal the delegate callbacks away from someone who really needs them, so in order to do this, you have to sort of ‘re-route’ the delegate callbacks. (If you’re implementing an infinite scroller, chances are you need to intercept / re-route the delegate anyway because I imagine you’ll create your own dataSource and delegate that are something like (note the conformance to the UIScrollViewDelegate):
@protocol HSCircularScrollViewDelegate<UIScrollViewDelegate> @optional - (void)circularScrollView:(HSCircularScrollView*)csv didMoveToItemAtIndex:(NSUInteger)itemIndex; - (void)circularScrollView:(HSCircularScrollView*)csv didTapOnItem:(UIView*)item atIndex:(NSUInteger)itemIndex; @end @protocol HSCircularScrollViewDataSource @required - (NSUInteger)numberOfItemsInCircularView:(HSCircularScrollView*)csv; - (UIView*)circularScrollView:(HSCircularScrollView*)csv viewForItemAtIndex:(NSUInteger)itemIndex; @end
But I digress…
Step 1 : Intercept the delegate
In your .m file’s Private Interface (Encapsulate!):
@interface HSCircularScrollView()<UIScrollViewDelegate> // conform to the delegate { __weak id<UIScrollViewDelegate> _myDelegate; // the delegate that other calling classes will set. BOOL _isSettingContentOffset; // We will need this later! } /* here we make scrolling have a public getter and a private setter. The accessors are automatically synthesized, which is GOOD, because these auto-synthesized methods generate KVO notifications whenever we use the dot notation. i.e. self.scrolling = YES; to change them. Free functionality! */ @property (nonatomic, assign, getter = isScrolling) BOOL scrolling; @end
I also hope to teach a bit of best practices, so please get in the habit of making your views be able to use Interface Builder by supporting the NSCoding protocol:
@implementation HSCircularScrollView - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupDefaults]; } return self; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder: aDecoder]; if (self) { [self setupDefaults]; } return self; } - (void)setupDefaults { _scrolling = NO; [super setDelegate: self]; // this is where we hijack the delegate. see the overridden method setDelegate: } - (id<UIScrollViewDelegate>)delegate { return _myDelegate; } - (void)setDelegate:(id<UIScrollViewDelegate>)aDelegate { // We are the delegate. We will always be the delegate. // We use our delegate callbacks to pass the message along // to the argument provided to this method [super setDelegate:self]; if (aDelegate != _myDelegate) { _myDelegate = aDelegate; } } // ... continued in next section
I have to stop midway here to make a few comments and then talk about the next bit. We’ve now just set up the infrastructure to be able to be the scrollView’s delegate so we can do some further required work of our own, but then can also pass on the delegate’s messages to any external class that needs this scrollView’s delegate functionality. Now that we have this, we can actually proceed to adding the task of knowing when the scroll view is scrolling.
Step 2 : Dealing with contentOffset
A UIScrollView
has a contentOffset
property that can be set directly, or can be animated. The issue is however, if you set the contentOffset and don’t animate it, there are still messages sent to the delegate’s scrollViewDidScroll:
callback. This is semantics. It basically means “scroll view did change its contentOffset”. However, for the purposes of knowing when a scrollView is visually scrolling (i.e. you see content moving across the screen), this method does not provide us with the info we need. We need to distinguish whether the contentOffset has changed because it animated or because it was ‘hard set’.
Now you will see above in the class’ private interface why there is a BOOL _isSettingContentOffset;
You need to do this any time your custom code calls:
_isSettingContentOffset = YES; self.contentOffset = somePoint;
(No, you can’t override setContentOffset and add this YES clause, because the UIScrollView itself calls this method internally as well when it actually is scrolling due to motion.)
Now we have to start implementing our UIScrollViewDelegate
methods:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { if (_isSettingContentOffset) { self.scrolling = NO; } else { self.scrolling = YES; } if (_isSettingContentOffset) { _isSettingContentOffset = NO; } // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { [_myDelegate performSelector:@selector(scrollViewDidScroll:) withObject:self]; } }
We’ve now dealt with the unpleasantness associated with contentOffset. Remember that if in your implementation of this subclass you have to programmatically change the contentOffset to change the _isSettingContentOffset = YES;
Step 3 : Complete the Hijacking process and add the scrolling KVO notifications
Now we simply look at the UIScrollViewDelegate methods and add the rest of the scrolling KVO notifications AS WELL AS ensuring all the delegate methods will be re-routed to any ‘real’ delegate.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) { self.scrolling = NO; } // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) { [_myDelegate scrollViewDidEndDragging:self willDecelerate:decelerate]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { self.scrolling = NO; // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) { [_myDelegate performSelector:@selector(scrollViewDidEndDecelerating:) withObject:self]; } } - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { self.scrolling = NO; // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewDidEndScrollingAnimation:)]) { [_myDelegate performSelector:@selector(scrollViewDidEndScrollingAnimation:) withObject:self]; } } // and now for completeness... - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) { [_myDelegate performSelector:@selector(scrollViewWillBeginDragging:) withObject:self]; } } - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView // called on finger up as we are moving { // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewWillBeginDecelerating:)]) { [_myDelegate performSelector:@selector(scrollViewWillBeginDecelerating:) withObject:self]; } } - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView { // check to see if I am my own delegate and then prevent infinite loop. if (_myDelegate != (id<UIScrollViewDelegate>)self && [_myDelegate respondsToSelector:@selector(scrollViewDidScrollToTop:)]) { [_myDelegate performSelector:@selector(scrollViewDidScrollToTop:) withObject:self]; } }
Long blog post, but hopefully this will be helpful to you. Create a subclass and add this stuff to it. Try it out by putting one of these in a view controller and have the view controller observe the scrolling property. And have the observe callback set a label’s text to scrolling or not scrolling. Then you’ll see.