Detect sudden scroll stop in UIScrollView - ios

I would like to know how to use the UIScrollViewDelegate to detect when the scroll view stops moving suddenly because the user has touched and held the screen after momentum has been initiated from a fast pan.
The scrollViewDidEndDecelerating: method only fires for the above case when the user has lifted their finger. However, if the user taps and holds during scroll view momentum then this method doesn't fire (until they lift their finger). Is there anyways to intercept this when the scroll view stops dead upon the user's touch down?

Have you tried using scrollViewWillBeginDragging? Alternatively (since the docs indicate that scrollViewWillBeginDragging may not fire immediately) you can try using scrollViewDidScroll and checking if the user is currently touching the scrollview...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if(scrollView.isTracking){
//do something
}
}

Well, you could have a flag that is raised when the user starts scrolling, which ends in scrollViewDidEndDecelerating. That way, if the user starts scrolling again before the flag is cleared, you will know that they touched it during a deceleration.

You don't have to implement your own.
Our friend Apple already provides you the way to detect the situation.
if needed for more, reference guide here : https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619436-scrollviewdidenddragging
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if decelerate {
print("true if scrolling stops, keeping touch on the screen!")
} else {
print("false if scrolling stops, detaching touch on the screen")
}
}

Related

Gracefully cancel UIScrollView touch (with animation)

There are myriad answers on cancelling a UIScrollView's touches (example SO question). However, the generally accepted answer (see prior example) is to do this by directly cancelling the scrollView's gesture recogniser, as below:
// Reset a scrollView's current 'touch'
scrollView.panGestureRecognizer.enabled = false
scrollView.panGestureRecognizer.enabled = true
The issue I'm currently having with this solution is that it cancels the ability for the scrollView to animate smoothly, and results in jumpy behaviour once you try to manipulate/animate the scrollView's contentOffset property.
In short, what I am trying to do is: once a paged scrollView reaches a certain content offset above its current page, cancel the current touch on the scrollView, animate back to the current page, and only then allow touches once more. You can liken this to having a scrollView containing a deck of cards that are only able to be scrolled through in one direction — if you attempt to go back (up) a card in the scrollView, you are stopped at a certain offset and the scrollView force-animates back down to the card you were on.
[Note — I understand there are other, more efficient ways to do this with gesture recognisers and custom views, but my need for a scrollView is more nuanced]
My code below (via my scrollView's delegate), cancels the current touch, but does not allow animation to happen. It simply 'snaps' back to place. Interestingly, without the pan gesture reset, the animation does happen — but of course since the pan gesture has not been reset the touch is allowed to continue dragging (even with userInteractionEnabled set to false).
// Delegate methods
func scrollViewDidScroll(scrollView: UIScrollView) {
if (scrollView.contentOffset.y <= (scrollView.frame.height PAGE_NUMBER - OFFSET_MARKER) {
// Where PAGE_NUMBER is the current page, and OFFSET_MARKER is the amount of pixels to trigger at, i.e. 80px
// Temporarily stop further user interaction
scrollView.userInteractionEnabled = false
// Reset the pan gesture recogniser -- THIS CAUSES 'SKIPPING' OF ANIMATION
scrollView.panGestureRecognizer.enabled = false
scrollView.panGestureRecognizer.enabled = true
// I have tried both 'animated: true / false'
UIView.animateWithDuration(0.4, animations: {
scrollView.setContentOffset(CGPointMake(0, self.frame.height), animated: true)
}, completion: { (complete) in
if complete{
// Re-enable user interaction (more touches)
scrollView.userInteractionEnabled = true
}
})
}
}
Any thoughts? Hopefully I'm missing something...
It turns out that the snapiness attributed to momentarily disabling the panGestureRecognizer stems from the scrollView being paged. Therefore, setting the scrollView.pagingEnabled property to false (temporarily in the delegate) solves it for me.
Not sure why it acts in this way, but I will do more digging and potentially file a bug report.

Handling UIPanGestureRecognizer gestures for multiple Views (one covers the other)

Sorry for such a long question, but felt I should convey what I have tried.
I've got a view viewA within a navigation controller. I am then adding a subview viewB (that contains a UITableView) to viewA and offsetting its origin height so that it covers only half the screen (with the other half overflowing off out the bottom of the screen). I want to be able to then drag this viewB upwards but it get stopped when it hits the bottom of the navigation bar and similarly get stopped when dragged back down when it hits the origin offset point. This I have achieved successfully.
However, I want the UITableView interaction to only be enabled when viewB is in its upper position and thus not respond to gestures in any other position. Essentially, dragging viewB up so that it completely covers viewA should enable interaction with the UITableView.
The tricky part here is that I want it to do the following:
If viewB is in its upper position so that it is covering the screen, the UITableView content offset is 0 (i.e. we are at the top of the table) and the user makes a pan gesture downwards, the gesture should not interact with the UITableView but should move viewB downwards.
Any other pan gesture in the above condition should be an interaction with the UITableView.
If viewB is in its upper position so that it is covering the screen, the UITableView content offset is NOT at 0 (i.e. we are NOT at the top of the table) and the user makes a pan gesture downwards, the gesture should interact with the UITableView.
I've been very close to achieving this but I can't get it quite right.
Attempts So Far
I'm using a UIPanGestureRecognizer to handle the dragging of the view. I have tried adding this to:
viewB with the UITableView user interaction initially disabled. This allows me to drag viewB up and down without interfering with the UITableView. Once viewB is in its upper position I enable UITableView user interaction which then correctly allows me to interact with the UITableView without moving viewB.
However, by enabling UITableView user interaction, this means touches never reach the UIPanGestureRecognizer, meaning I can never detect for the scenario described in point (1.) above and thus can't re-disable UITableView user interaction to make viewB movable again.
Maybe it is possible to do it this way by overriding the gesture recognition methods used by the UITableView? If this is possible can anyone point me in the right direction?
a new view added in front of the UITableView. I thought maybe I could forward the touch gestures to the UITableView behind it when necessary but I still haven't found a way to do this.
All I have been able to do is disable the gesture recognizer which allows me to interact with the UITableView, but then I have the same issue as above. I can't detect when to re-enable it.
the UITableView within viewB. This seemed to be the most promising way so far. By setting the return values of the following methods I can enable and disable recognition of either viewB and the UITableView.
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldReceiveTouch touch: UITouch) -> Bool {
if pulloverVC.view.frame.origin.y == bottomNavbarY &&
pulloverVC.tableView?.contentOffset.y == 0 { // need to add gesture direction check to this condition
viewBisAtTop = true
return false // disable pullover control
}
return true // enable pullover control
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if (gestureRecognizer as! UIPanGestureRecognizer).velocityInView(view).y < 0 && viewBisAtTop { // gesture direction check not wanted here
return true // enable tableview control
}
viewBisAtTop = false
return false // disable tableview control
}
The top method is called first when a gesture is made (I have checked with print statements) followed by the bottom method. By making different combinations of true/false for the 2 methods I can alternate interaction between viewB and the UITableView.
To detect whether the user is swiping downwards I am calling velocityInView() on the recognizer (as shown in the bottom method). I was intending on making this check in the top methods if statement and I think this would work, however, although velocityInView() works fine in the bottom method, it does not in the top one (velocity is always 0).
I have scoured SO for some solution and find many similar queries about gesture handling for views that cover each other, but these all seem to be regarding one gesture type, e.g. pinch, on one view, and another type, e.g. pan, on the other. In my case the gesture type is the same for both.
Maybe someone has a clever idea? Or maybe this is actually very simple to do and I have made this incredibly complicated? xD
Managed to get this working.
Of the methods described in my question above I removed the top one keeping just this (it has a few changes):
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if ((gestureRecognizer as! UIPanGestureRecognizer).velocityInView(view).y < 0
|| pulloverVC.tableView.contentOffset.y > 0)
&& pulloverVC.view.frame.origin.y == bottomNavbarY {
return true // enable tableview control
}
return false
}
The if statement checks that the covering UITableView is in its upper position AND that either the user is is not dragging downwards or the table content is offset (we are not at the top of the table). If this is true, then we return true to enable the tableview.
After this method is called, the standard method implemented to handle my pan gesture is called. In here I have an if statement that sort of checks the opposite to above, and if that's true, it prevents control over the covering viewB from moving:
func handlePanGesture(recognizer: UIPanGestureRecognizer) {
let gestureIsDraggingFromTopToBottom = (recognizer.velocityInView(view).y > 0)
if pulloverVC.view.frame.origin.y != bottomNavbarY || (pulloverVC.view.frame.origin.y == bottomNavbarY && gestureIsDraggingFromTopToBottom && pulloverVC.tableView.contentOffset.y == 0) {
...
This now keeps the UITableView interaction off unless its parent view viewB is in the correct position, and when it is, disables the movement of viewB so that only interaction with the UITableView works.
Then when, we are at the top of the table, and drag downwards, interaction with the UITableView is re-disabled and interaction with its parent view viewB is re-enabled.
A wordy post and answer, but if someone can make sense of what I'm saying, hopefully it will help you.

Can I disable / edit the automatic jump-to-top scroll when tapping status bar?

I'm using an app with a tableView that auto-scrolls downward, so tapping the status bar, which would normally jump to the top of the table, causes problems (it begins scrolling but if the auto-scroll ticks, it stops and leaves it somewhere in the middle).
I'd either like to disable it, or at least have shove some code in when it's tapped to temporarily pause the timer and then resume it when it reaches the top.
Are there any means of achieving either of these things?
UIScrollView (and UITableView as a subclass of it) has scrollsToTop property which defaults to YES, making it scroll to top when status bar is tapped. All you have to do is set it to NO.
However, please keep in mind that iOS users might expect this behavior, so turning it off may not be the best idea from user experience standpoint. You can also leave it as YES and return NO from -scrollViewShouldScrollToTop: delegate method if you only need it disabled at specific times.
Actually, handling it via delegate might be a perfect fit for your case:
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
// disable timer
return YES;
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
// re-enable timer
}
You can try:
[myView setScrollsToTop:NO];
For swift 5
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return false
}

scrollViewDidEndScrollingAnimation not getting called

I want to perform some action when scrollview finish scrolling, so I wrote that acton in scrollViewDidEndScrollingAnimation delegate method. It is working fine when rect is not visible and scrollview scrolls to new rect. But when rect is already visible scrollViewDidEndScrollingAnimation method will not be called and so the method written inside scrollViewDidEndScrollingAnimation will not get called. But I want to call that action, can anyone knows how to call that method when scrollview finish scrolling?
Thanks in advance!
I think this one catches a few people out. What actually happens is that scrollViewDidEndScrollingAnimation is only called if you explicitly invoke either the setContentOffset:animated: or the scrollRectToVisible:animated: methods.
As the UIScrollViewDelegate Protocol Reference states:
Discussion
The scroll view calls this method at the end of its
implementations of the setContentOffset:animated: and
scrollRectToVisible:animated: methods, but only if animations are
requested.
So what to do? Well, let's not forget that typically the offsetting of content data in a scroll view is not animated. Rather, it is the result of continuously updating the contentOffset value. So, you could possibly trigger your method based upon a specific contentOffset using the scrollViewDidScroll: delegate method.
Alternatively, if it is something to be done after every scroll gesture - specifically, after the private UIScrollViewPanGestureRecognizer - then you could do it in scrollViewDidEndDecelerating::
Discussion The scroll view calls this method when the scrolling
movement comes to a halt. The decelerating property of UIScrollView
controls deceleration.
Implement both scrollViewDidEndDecelerating: and scrollViewDidEndDragging:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// User lifted finger while scrolling
[self doPostScrollAction];
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if(!decelerate)
{
// User lifted finger after stopping scrolling
[self doPostScrollAction];
}
}

UIScrollView scrollRectToVisible:animated: is there a way that a method can be called when animation ends

Is there a way to know when the animation has end and uiscrollview has come to rest.
Yup use scrollViewDidEndScrollingAnimation
I do it like this because sometimes using the delegate isn't practical for me, like if i'm doing it in UIViewController transition:
[UIView animateWithDuration:0.3 animations:^{
[scrollView setContentOffset:CGPointMake(0, -scrollView.contentInset.top) animated:NO];
} completion:^(BOOL finished) {
// This is called when it's complete
}];
Implement UIScrollViewDelegate delegate methods for your UIScrollView the following way:
Use scrollViewDidEndScrollingAnimation: to detect when the scrolling animation concludes when you've initiated the scrolling by calling setContentOffset:animated: or scrollRectToVisible:animated: methods (with animated:YES).
If you want to monitor scroll view motion that's been initiated by touch gestures, use scrollViewDidEndDecelerating: method, which is called when the scrolling movement comes to a halt.
You need to cover THREE (!) cases. Thanks, Apple.
// do note that you need all three of the following
public func scrollViewDidEndScrollingAnimation(_ s: UIScrollView) {
// covers case setContentOffset/scrollRectToVisible
fingerOrProgrammaticMoveDone()
}
public func scrollViewDidEndDragging(_ s: UIScrollView, willDecelerate d: Bool) {
if decelerate == false {
// covers certain cases of user finger
fingerOrProgrammaticMoveDone()
}
}
public func scrollViewDidEndDecelerating(_ s: UIScrollView) {
// covers certain cases of user finger
fingerOrProgrammaticMoveDone()
}
(Be careful to not forget the extra "if" clause in the middle one.)
Then in fingerOrProgrammaticMoveDone() , do what you need.
A good example of this is the nightmare of handling paged scrolling. It is very, very hared to know what page you are on.
scrollViewDidEndDecelerating: UIScrollView delegate method is called when scrollView stops completely.

Resources