iOS: Cancel UIScrollView touches when using 2 fingers - ios

I have written a UIScrollView subclass that I am using to scroll a series of UITableViews. See the following diagram:
As you can see I have several vertically scrolling UITableViews, that are being scrolled horizontally inside a parent UIScrollView. This all works fine. However the application has a number of global gestures. For example, if I swipe in a given direction with 2 fingers, I do a UIView transition to another part of the app. but if I do the gesture on top of the scroll view and/or its child table views, they naturally scroll their content. This doesn't look good and causes some layout issues.
What I would like to figure out is how to disable all scrolling, on both the UIScrollView and its child UITableViews, when a user touches anywhere with two fingers, and only with two fingers. I've tried variations of overriding touchesBegan, touchesEnded, touchesShouldCancel etc... but I can't get it quite right. Any help is much appreciated.
Here is my gesture handling code:
UISwipeGestureRecognizer *twoFingerSwipeUp = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(handleTwoFingerSwipe:)];
[twoFingerSwipeUp setNumberOfTouchesRequired:2];
[twoFingerSwipeUp setDirection:UISwipeGestureRecognizerDirectionUp];
[twoFingerSwipeUp setDelegate:self];
// 'self' is the superview of the UIScrollView, which is a UIView.
[self addGestureRecognizer:twoFingerSwipeUp];
[twoFingerSwipeUp release];
// ... repeat the above code for up, down, left, right gestures ...
- (void)handleTwoFingerSwipe:(UISwipeGestureRecognizer*)swipeGesture {
switch ([swipeGesture direction]) {
case UISwipeGestureRecognizerDirectionUp:
[self changeToView:viewAbove];
break;
case UISwipeGestureRecognizerDirectionDown:
[self changeToView:viewBelow];
break;
case UISwipeGestureRecognizerDirectionRight:
[self changeToView:viewToTheRight];
break;
case UISwipeGestureRecognizerDirectionLeft:
[self changeToView:viewToTheLeft];
break;
}
}

Try setting panGestureRecognizer.maximumNumberOfTouches = 1 on all scroll and table views (iOS 5 only).

If you're using a swipe recogniser for the two-finger swipe, require the recognisers of the scroll view (including the table views — they're scroll view as well) to fail when the two-finger recogniser recognises its gesture.
[[scrollView panGestureRecognizer] requireGestureRecognizerToFail: twoFingerRecogniser];
Iterate the above code for every scroll view and table view.
(P.S.: "recogniser" is British English, not a spelling err.)
Hope that helps. :-)

Write this code:
scrollView.minimumZoomScale=1.0;scrollView.maximumZoomScale=1.0;
scrollView.delegate self];
And Here is scrollViewDelegate Method:-
-(UIView*)viewForZoomingInScrollView:(UIScrollView *)aScrollView{
return aScrollView;}

One thing that you should be doing is to check that the gesture has finished before acting upon it:
if (swipeGesture.state == UIGestureRecognizerStateEnded) {
// Do your think
}
I've known odd things to happen otherwise.

Just disable user interaction in the parent scroll view. You need a UIWindow subclass and override -sendEvent: method because this gets called BEFORE any gesture recognizer. There, if you detect two touches, send a notification. Let the scroll view listen to it and disable user interaction if it occurs. And if touches ended, let it re-enable user interaction.

Related

How to dismiss programmatically the clipsToBounds property when a GestureRecognizer begin?

I have a UIScrollView which is able to contains many view. To allow a good scrolling (without the content going outside the view while scrolling), on my Main.sotryboard , I've clicked on my UIScrollView and then in the attribute inspector I have allowed the Clip Subviews property:
My problem: all the views which are in my UIScrollViews are draggable (because they all have a UIPanGestureRecognizer.
So, when I try to drag them OUTSIDE my UIScrollView, they just disappear.
In fact they're just going behind every other view
To give you an exemple, I have others components which allow the drop of a view form the precedent UIScrollView. So when I begin the drag'n'drop from it, it disappear, and then reappear in the second component on which I have dropped the view.
What I have tried: I have a special UIPanGestureRecognizer for te drag'n'drop of a view coming from this UIScrollView. So, I actually have this (which, obviously, doesn't work, otherwise I would not be here):
//Here recognizer is the `UIPanGestureRecognizer`
//selectpostit is the name of the view I want to drag
if(recognizer.state == UIGestureRecognizerStateBegan){
selectpostit.clipsToBounds = NO;
}
Any Ideas on how I can improve that?
Thanks in advance.
You could try to reset scrollView.clipsToBounds to NO every time gesture starts, but that would lead to side effect when other content outside scroll view would become visible when dragging is in the progress.
I would recommend to take snapshot of the the draggable view when panning starts, place it on the scrollview's parent, and move it. Such approach should solve your problem.
Here is the code:
- (void)onPanGesture:(UIPanGestureRecognizer*)panRecognizer
{
if(panRecognizer.state == UIGestureRecognizerStateBegan)
{
//when gesture recognizer starts, making snapshot of the draggableView and hiding it
//will move shapshot that's placed on the parent of the scroll view
//that helps to prevent cutting by the scroll view bounds
self.draggableViewSnapshot = [self.draggableView snapshotViewAfterScreenUpdates: NO];
self.draggableView.hidden = YES;
[self.scrollView.superview addSubview: self.draggableViewSnapshot];
}
//your code that updates position of the draggable view
//updating snapshot center, by converting coordinates from draggable view
CGPoint snapshotCenter = [self.draggableView.superview convertPoint:self.draggableView.center toView: self.scrollView.superview];
self.draggableViewSnapshot.center = snapshotCenter;
if(panRecognizer.state == UIGestureRecognizerStateEnded ||
panRecognizer.state == UIGestureRecognizerStateCancelled ||
panRecognizer.state == UIGestureRecognizerStateFailed)
{
//when gesture is over, cleaning up the snapshot
//and showing draggable view back
[self.draggableViewSnapshot removeFromSuperview];
self.draggableViewSnapshot = nil;
self.draggableView.hidden = NO;
}
}
I recommend you look at this article by ray wenderlich Moving Table View Cells with a Long Press Gesture
It explains how to create snapshots

UIScreenEdgePanGestureRecognizer Triggering Multiple Times

I have the following code in a viewDidLoad on a UIViewController:
UIScreenEdgePanGestureRecognizer *edgeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:#selector(handleRightEdgeSwipe:)];
edgeRecognizer.edges = UIRectEdgeRight;
[self.view addGestureRecognizer:edgeRecognizer];
and the purposes is to trigger a view to slide in when a right edge gesture is detected.
-(void)handleRightEdgeSwipe:(UIGestureRecognizer*)sender
{
NSLog(#"Showing Side Bar");
[self presentPanelViewController:_lightPanelViewController withDirection:MCPanelAnimationDirectionRight];
}
But I am seeing that the "handleRightEdgeSwipe" function is triggered multiple times - sometimes 5 times which makes the side bar view that should smoothly animate slide in to flash multiple times.
(NOTE: I tried triggering the view to appear from a UIButton and it works fine).
Why is the right edge gesture triggered multiple times and how can I fix it?
As noted, the UIScreenEdgePanGestureRecognizer invokes your action multiple times as the state of the GestureRecognizer changes. See the documentation for the state property of the UIGestureRecognizer class. So, in your case I believe the answer you're looking for is to check if the state is "ended". Thus:
-(void)handleRightEdgeSwipe:(UIGestureRecognizer*)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
NSLog(#"Showing Side Bar");
[self presentPanelViewController:_lightPanelViewController withDirection:MCPanelAnimationDirectionRight];
}
}
This gesture is not a single-shot event but continuous.
handleRightEdgeSwipe: is called once whenever sender.state changes or the touch moved around. You have to move the UIButton depending on the gesture's state and locationInView:.

Delay the scrolling in a UIScrollView by changing the required finger offset

I want to make my scrollview less sensitive. This means that I want the user to have to move the finger a bit more before the view starts scrolling. Does anyone knows how to achieve this? I remember seeing a property that would let me set a required number of pixels before a scroll is detected. But it could just be my imagination. Perhaps what is required is to modify the underlying UIPanGestureRecognizer inside the scroll view?
The reason for this is that I am detecting a press on the view, so as long as the user has the finger there a function is constantly running. This function has to have the absolute priority, but the user might want to scroll the view instead, so I am canceling the function by detecting if the scrollview was scrolled. Everything works perfectly, except when the user is moving his hand/arm, his finger "might slip" a little bit, thus canceling the function since the scrollview starts scrolling.
Edit: (to address some of the confusion in the question)
How do I delay the scrolling in a scrollview by requiring the user to move the finger more before the scrollview starts scrolling?
I write an Answer so i can write code, but i am not entirely sure why you want the finger-offset. Lets assume you have an Area on your scrollview in which the user can longpress. During longpress you want to do some calculating and you do not want the scrollview to move. In my opinion there are two possibilties: Either you want the longpress to cancel if the finger moves a certain amount ("difficult to archive") or you want to supress the scrollview movement as long as the finger is pressed down, no matter what the movement is ("simple to archive").
Simple Solution:
- (void)yourUIBuilder {
...
UILongPressGestureRecognizer *recognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(longPressChanged:)];
[recognizer setDelegate:self];
[recognizer setMinimumPressDuration:0.3];
[yourScrollView addGestureRecognizer:recognizer];
}
- (void)longPressChanged:(UIGestureRecognizer *)recognizer {
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
// start your calculations
break;
case UIGestureRecognizerStateChanged:
break;
default:
// stop your calculations
break;
}
}

Cross Directional UIScrollViews - Can I Modify the Scrolling Behaviour?

Here's how the scroll views work: One scroll view is paging enabled in the horizontal direction. Each 'page' of this scroll view contains a vertically scrolling UITableView. Without modification, this works OK, but not perfectly.
The behaviour that's not right: When the user scrolls up and down on the table view, but then wants to flick over to the next page quickly, the horizontal flick/swipe will not work initially - it will not work until the table view is stationary (even if the swipe is very clearly horizontal).
How it should work: If the swipe is clearly horizontal, I'd like the page to change even if the table view is still scrolling/bouncing, as this is what the user will expect too.
How can I change this behaviour - what's the easiest or best way?
NOTE For various reasons, a UIPageViewController as stated in some answers will not work. How can I do this with cross directional UIScrollViews (/one is a table view, but you get the idea)? I've been banging my head against a wall for hours - if you think you can do this then I'll more than happily award a bounty.
According to my understanding of the question, it is only while the tableView is scrolling we want to change the default behaviour. All the other behaviour will be the same.
SubClass UITableView. UITableViews are subClass of UIScrollViews. On the UITableView subClass implement one UIScrollView's UIGestureRecognizer's delegate method
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer
{
//Edit 1
//return self.isDecelerating;
//return self.isDecelerating | self.bounces; //If we want to simultaneous gesture on bounce and scrolling
//Edit 2
return self.isDecelerating || self.contentOffset.y < 0 || self.contentOffset.y > MAX(0, self.contentSize.height - self.bounds.size.height); // #Jordan edited - we don't need to always enable simultaneous gesture for bounce enabled tableViews
}
As we only want to change the default gesture behaviour while the tableView is decelerating.
Now change all 'UITableView's class to your newly created tableViewSubClass and run the project, swipe should work while tableView is scrolling. :]
But the swipe looks a little too sensitive while tableView is scrolling. Let's make the swipe a little restrictive.
SubClass UIScrollView. On the UIScrollView subclass implement another UIGestureRecognizer's delegate method gestureRecognizerShouldBegin:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]) {
CGPoint velocity = [(UIPanGestureRecognizer *)gestureRecognizer velocityInView:self];
if (abs(velocity.y) * 2 < abs(velocity.x)) {
return YES;
}
}
return NO;
}
We want to make the "swipe is clearly horizontal". Above code only permits gesture begin if the gesture velocity on x axis is double than on y axis. [Feel free to increase the hard coded value "2" if your like. The higher the value the swipe needs to be more horizontal.]
Now change the `UiScrollView' class (which has multiple TableViews) to your ScrollViewSubClass. Run the project. :]
I've made a project on gitHub https://github.com/rishi420/SwipeWhileScroll
Although apple doesn't like this method too much:
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
I've found a great way to accomplish this.
This is a complete solution for the problem. In order to scroll the UIScrollView while your UITableView is scrolling you'll need to disable the interaction you have it.
- (void)viewDidLoad
{
[super viewDidLoad];
_myScrollView.contentSize = CGSizeMake(2000, 0);
data = [[NSMutableArray alloc]init];
for(int i=0;i<30;i++)
{
[data addObject:[NSString stringWithFormat:#"%d",i]];
}
UITapGestureRecognizer * tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.view addGestureRecognizer:tap];
}
- (void)handleTap:(UITapGestureRecognizer *)recognizer
{
[_myTableView setContentOffset:_myTableView.contentOffset animated:NO];
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
scrollView.userInteractionEnabled = YES;
}
To sum up the code above, if the UITableView is scrolling, set userInteractionEnabled to NO so the UIScrollView will detect the swipe. If the UITableView is scrolling and the user taps on the screen, userInteractionEnabled will be set to YES.
Instead of using UIScrollView as a container for these multiple table views, try using a UIPageViewController.
You can even integrate this into your existing view controller setup as a child view controller (directly replacing the UIScrollView).
In addition, you'll likely want to implement the required methods from UIPageViewControllerDataSource and possibly one or more of the methods from UIPageViewControllerDelegate.
Did you try the methods : directionalLockEnabled of both your table and scroll and set them up to horizontal for one and vertical for the other ?
Edit :
1)
What you want to do is very complicate since the touch wait some time (like 0.1s) to know what your movement will be. And if your table is moving, it will take your touch immediately whatever it is (because it's suppose to be reactive movement on it).
I don't see any other solution for you but to override touch movement from scratch to detect immediately the kind of mouvement you want (like if the movement will be horizontal) but it will be more than hard to do it good.
2)
Another solution I can advise you is to make your table have left and right margin, where you can touch the parent scroll (pages thing so) and then even if your table is scrolling, if you touch here, only your paging scroll will be touched. It's simpler, but could not fit with your design maybe...
Use UIPageViewController and in the -viewDidLoad method (or any other method what best suits your needs or design) get UIPageViewController's UIScrollView subview and assign a delegate to it. Keep in mind that, its delegate property won't be nil. So optionally, you can assign it to another reference, and then assign your object, which conforms to UIScrollViewDelegate, to it. For example:
id<UIScrollViewDelegate> originalPageScrollViewDelegate = ((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]).delegate;
[((UIScrollView *)[pageViewController.view.subviews objectAtIndex:0]) setDelegate:self];
So that you can implement UIScrollViewDelegate methods with ease. And your UIPageViewController will call your delegate's -scrollViewDidScroll: method.
By the way, you may be obliged to keep original delegate, and respond to delegate methods with that object. You can see an example implementation in ViewPagerController class on my UI control project here
I faced the same thing recently. My UIScrollview was on paging mode and every page contained a UITableView and like you described it worked but not as you'd expected it to work. This is how solved it.
First I disabled the scrolling of the UIScrollview
Then I added a UISwipeGestureRecognizer to the actual UITableView for left and right swipes.
The action for those swipes were:
[scroll setContentOffset:CGPointMake(currentPointX + 320, PointY) animated:YES];
//Or
[scroll setContentOffset:CGPointMake(currentPointX - 320 , PointY) animated:YES];
This works flawlessly, the only down side is that if the user drags his finger on the UITableVIew that will be considered as a swipe. He won't be able to see half of screen A and half of screen B on the same screen.
You could subclass your scroll view and your table views, and add this gesture recognizer delegate method to each of them...
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
I can't be sure this is exactly what you are after, but it may come close.

Detecting swipe gestures on UITableViewCell inside UIScrollView

I am hoping someone will be able to help me with a problem that is doing my head in at the moment!
Given the following view hierarchy
I want to be able to detect swipe gestures on my custom UITableViewCell.
I have subclassed the UIScrollView and have a hitTest:withEvent: method that checks whether I am touching the tableview cell (or its content) or not, in which case I set the following scroll view properties:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if ([result.superview isKindOfClass:[UITableViewCell class]] || [result.superview tag] == SUBVIEW_TAG)
{
self.canCancelContentTouches = NO;
self.delaysContentTouches = YES;
} else {
self.canCancelContentTouches = YES;
self.delaysContentTouches = NO;
}
return result;
}
I have also implemented:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if (view.tag == SUBVIEW_TAG || [[view superview] isKindOfClass:[UITableViewCell class]])
return NO;
return YES;
}
And am returning NO in case the view being touched is the table view cell.
These methods are all getting called and performing their actions as expected, but I am still unable to stop the UIScrollView from "hogging" the swipe gesture.
The interesting thing is that if I include the UIView that contains the tableview and cell on both of the methods above (the one with SUBVIEW_TAG) it works perfectly so I am guessing it must be something to do with the fact that UITableView inherits from UIScrollView.
My main goal is to be able to swipe on the cell to reveal more options for the cell. A horizontal swipe anywhere else on that view would be captured by the scroll view and shift the content horizontally as per its normal behaviour.
Any ideas would be very much appreciated!
Thanks!
Rog
I had a similar problem with a swipe detect for a component inside a scrollview and I was able to resolve it with
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:swipeGesture]
Where scrollView is the scroll view object that acts like container and swipeGesture is the component swipe gesture object inside scrollview.
So, you can define a swipe for the cell object like this (for right swipe in the example, custom it as you want)
UISwipeGestureRecognizer* rightSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(yourMethod)];
[rightSwipeRecognizer setDirection:UISwipeGestureRecognizerDirectionLeft];
[cell addGestureRecognizer:rightSwipeRecognizer];
and then do
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:rightSwipeRecognizer]
The documentation of requireGestureRecognizerToFail says:
This method creates a relationship with another gesture recognizer
that delays the receiver’s transition out of
UIGestureRecognizerStatePossible. The state that the receiver
transitions to depends on what happens with otherGestureRecognizer:
If otherGestureRecognizer transitions to
UIGestureRecognizerStateFailed, the receiver transitions to its normal
next state.
if otherGestureRecognizer transitions to
UIGestureRecognizerStateRecognized or UIGestureRecognizerStateBegan,
the receiver transitions to UIGestureRecognizerStateFailed.
An example where this method might be called is when you want a
single-tap gesture require that a double-tap gesture fail.
Availability Available in iOS 3.2 and later.
Hope helps!
The solution is pretty simple. All you need to do is add UIScrollView inside you UITableViewCell. It will prevent "hogging" effect during swipe gesture.

Resources