I'm working on an app where I have several UIView objects that are subviews on a UIScrollView object. I create the subviews programmatically and place them on the scroll view according to the properties of associated objects. The user is allowed to move these subviews around on the scroll view. Usually this works, but sometimes the scrollview grabs the pan gesture.
What I'd like to do is to suppress the scroll view gesture recognizer if the touch location is inside one of the subviews.
I can find the scroll view gesture recognizer by looking through the scroll view's array of gesture recognizers and looking for a UIScrollViewPanGestureRecognizer object. I assume there can only be one.
An idea I have is to make my view controller be a delegate of this gesture recognizer and then have the delegate suppress it if the touch is within the bounds of one of the subviews.
Is this the best way to handle this scenario, or is there a better way?
I've done something similar, described in my answer to my own question here.
How to get stepper and longpress to coexist?
Hmmm. Looks like it will be more difficult than I anticipated to recognize the scrollview's UIScrollViewPanGestureRecognizer. Any hints on doing this would be appreciated.
My idea doesn't work. In order to code my idea, I had to make my VC be the delegate of the scrollview's pan gesture recognizer. However, when I do that, I get this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'UIScrollView's built-in pan gesture recognizer must have its scroll view as its delegate.'
Here is the code I used. In viewDidLoad I called a method which got the scrollview's pan gesture recognizer and set self as delegate (self.scrollViewPanGestureRecognizer is just a property to store it):
self.scrollViewPanGestureRecognizer = [self.scrollView panGestureRecognizer];
self.scrollViewPanGestureRecognizer.delegate = self;
I then implemented this delegate method:
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
//Disable touch if touch location is in a subview.
BOOL enableGestureRecognizer = YES;
if (gestureRecognizer == self.scrollViewPanGestureRecognizer) {
CGPoint touchLocation = [touch locationInView:self.scrollView];
for (UIView *s in self.scrollView.subviews) {
if (CGRectContainsPoint(s.frame, touchLocation)) {
enableGestureRecognizer = NO;
}
}
}
return enableGestureRecognizer;
}
Seemed like a good idea, but it looks like I can't make my VC be the delegate.
Just tried setting scrollEnabled to NO on the scroll view. That successfully disabled scrolling, but it did not fix the problem. Views still occasionally do not respond to gestures. Thinking that perhaps some bug caused the gesture recognizer to fall off the object, I asked the debugger to display the gesture recognizers for the problematic views. They were still there. I'm stumped.
UPDATE: New information. I finally realized that the subviews that aren't responding are the ones on the right side of the screen. After carefully testing, it seems that this happens only in landscape orientation and only when the finger location is to the right of the right edge in portraite, i.e. 320 points. Apparently, something is not being handled property when rotating to landscape. Everything appears normal, but the gestures aren't being recognized.
Just for grins, I decided to display the frames and bounds and content area in the method viewDidLayoutSubviews. What I get is:
self.view.frame is {{0, 0}, {480, 320}}
self.view.bounds is {{0, 0}, {480, 320}}
self.scrollView.frame is {{0, 0}, {480, 320}}
self.scrollView.bounds is {{0, 0}, {480, 320}}
self.scrollView.contentSize is {480, 320}
I seem to have missed something. What else needs to be set when rotating?
use requireGestureRecognizerToFail: method.
you want your scroll view pan gesture (scrollViewGesture) to be failed when one of the gestures happen on its subView.
So, when you add pan gesture to your subView (subViewGesture), set below property as
scrollViewGesture.requireGestureRecognizerToFail =subViewGesture;
I found the solution. I'd forgotten that the subviews are not placed directly into the scroll view. There is a view originally occupying the bounds of scrollview onto which the subviews are placed. The hierarchy is like this:
self.view
scroll view
UIView (fills whole scroll view)
subview1
subview2
subviewn
In my code to handle rotation, I was not resizing the UIView into which the subviews are placed. Correcting this issue solved the problem.
I'd originally tried placing the subviews without their UIView superview in between them and the scroll view, but it didn't work for some reason. Adding this extra layer solved that problem, but I forgot to handle the resizing when rotating.
So I guess the gesture recognizers did not respond because although they were visible, they were outside the bounds of their superview.
I'm making this answer a community wiki because I haven't completely worked out this solution yet. The main thing is to take advantage of this from the documentation:
Subclasses can override the
touchesShouldBegin:withEvent:inContentView:, pagingEnabled, and
touchesShouldCancelInContentView: methods (which are called by the
scroll view) to affect how the scroll view handles scrolling
gestures.
One solution: instead of playing around with gesture recognizers, just disable all of them and use touchesBegan, touchesMove and touchesEnded directly. It might be a bit of work, but pretty sure it will work exactly the way you want.
You need to disable user interaction on the subviews, disable scrolling on the scrollview, and modify the scrollview's contentOffset directly.
Related
I have added a subview to a View Controller's view. This subview is the view of QLPreviewController.
What I am trying to achieve is to recognize swipe gestures on the subview in the parent view, i.e. the View Controller's view. In the end, I want to be able to swipe left /right on the view to load the next document for preview.
I'm aware of hit testing and understand that by just attaching a gesture recognizer to the parent view, those will not be recognized, since the subview will be the "hit-test" view.
Now what is the best (or easiest) way to recognize those gestures?
Note: I didn't manage to attach the gesture recognizers to the subview, this doesn't seem to work.
* UPDATE *
To make this more clear - this is the code from my ViewController. vContent is just a view in my ViewController, where I add the view of the QLPreviewController:
let pvVc = QLPreviewController()
pvVc.dataSource = self
vContent.addSubview(pvVc.view)
I tried adding the swipe recognizers both to the vContent and the pvVc.view. In both cases no event was fired.
let sgrLeft: UISwipeGestureRecognizer = UISwipeGestureRecognizer(target: self, action:Selector("handleSwipe:"))
sgrLeft.direction = UISwipeGestureRecognizerDirection.Left
sgrLeft.delegate = self
On some other view the code works fine.
Any hint is appreciated!
Thx
Eau
Well, the responder chain, the unknown animal … ;-)
You can subclass the superview and override -hitTest:forEvent:.
You rarely need to call this method yourself, but you might override it to hide touch events from subviews.
Gesture Recognizers Get the First Opportunity to Recognize a Touch, so even the subview is hitTest view. the gestureRecognizer attached on superView can recognizer touch event.
I have UIPageViewController that animates programatically. The problem is that the view controllers inside it has UIButtons inside them. When I hold down a button and wait until the UIPageViewController animates, the app crashes with the error:
'Failed to determine navigation direction for scroll'
What I think I need to do is to somehow fake that the user releases the button before the UIPageviewController animates.
However, [self.button sendActionsForControlEvents:UIControlEventTouchCancel]; doesn't seem to do the trick. Neither do UIControlEventTouchUpInside.
Is there a better way do to it or am I using sendActionsForControlEvents wrong?
All sendActionsForControlEvents: does is call any methods you've assigned to the control events passed in for the button. It doesn't call any internal methods to programmatically lift up touches or anything like that.
Right before you programmatically animate your page view controller, try using this method to effectively cancel any touches on the pan gesture recognizer of the page view controller's internal scroll view:
- (void)cancelPanGestureTouchesOfPageViewController:(UIPageViewController *)pageVC
{
// Since UIPageViewController doesn't provide any API to access its scroll view,
// we have to find it ourselves by manually looping through its view's subviews.
for (UIScrollView *scrollView in pageVC.view.subviews) {
if ([scrollView isKindOfClass:[UIScrollView class]]) {
// We've found the scroll view, so use this little trick to
// effectively cancel any touches on its pan gesture recognizer
BOOL enabled = scrollView.panGestureRecognizer.enabled;
scrollView.panGestureRecognizer.enabled = !enabled;
scrollView.panGestureRecognizer.enabled = enabled;
}
}
}
(Note that this is messing with the internal view hierarchy of UIPageViewController, so this method is kind of ugly and may break in the future. I generally don't recommend doing stuff like this, but I think in this instance it should be okay.)
I have two UIScrollViews on my screen and I need to be able to drag a UIView from one scrollview to the other.
At the moment, I have a UILongGestureRecognizer on the UIView that I want to move, such that when the user starts dragging it, I make the view follow the touch:
- (void)dragChild:(UILongPressGestureRecognizer *)longPress {
[[longPress view] setCenter:[longPress locationInView:[[longPress view] superview]]];
}
But when I get the boundary of the starting UIScrollView, the view disappears because it's locked into that scrollview's bounds.
Is there a way to "pop" it out of the scrollview when I start dragging such that I can carry it over to the other scrollview?
Also, how do I test for the "drop" location? I want to know if it's been dropped over a certain other view.
Or am I going about this all the wrong way?
Thanks guys
If you will need to drag it from a scroll view to another do the following (pseudo code)
when you start dragging do the following
//scrollView1 is the scroll view that currently contains the view
//Your view is the view you want to move
[scrollView1.superView addSubView:yourView];
now when dropping the view, you will need to know if it is inside the other scrollview
//scrollView2 is the second scroll view that you want to move it too
//Your view is the view you want to move
CGPoint point = yourView.center;
CGRect rect = scrollView2.frame;
if(CGRectContainsPoint(rect, point))
{
//Yes Point is inside add it to the new scroll view
[scrollView2 addSubView:yourView];
}
else
{
//Point is outside, return it to the original view
[scrollView1 addSubView:yourView];
}
Here is an untested idea:
When the drag begins, move the dragging-view out of the scroll view (as a subview) and into the mutual superview of both scroll views.
When the drag ends, move the dragging-view out of the superview and into the new scroll view.
You'll probably have to be careful with coordinate systems, using things like [UIView convertPoint:toView:] to convert between the views' different perspectives when moving things around.
I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}
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.