UIGestureRecognizer with a touch moving off UIView - ios

I have a UILongPressGestureREcognizer with a minimum press length of 0 seconds. On UIGestureRecognizerStateStart I alter the UIView. On UIGestureRecognizerStateEnded, I run the code for what ever the press is suppose to do.
-(IBAction)longPressDetected:(UILongPressGestureRecognizer *)recognizer {
static NSTimer *timer = nil;
if (recognizer.state == UIGestureRecognizerStateBegan) {
// alter look of UIView
}
else if (recognizer.state == UIGestureRecognizerStateEnded) {
// do something
// change look back to original state
}
}
I am running into an issue where if the touch starts on the UIView and if the touch is dragged outside of the UIView and is lifted, the UIGestureRecognizerStateEnded still fires.
I need to be able to handle a touch inside the UIView, the user drag outside of the UIView and to cancel it.
I know a UIButton can do it, but it does not fulfill all the needs I have.
How can I cancel a touch if the touch is dragged outside of the UIView?

Add checking of touch location. This method can be heplfull.
- (CGPoint)locationInView:(UIView *)view

Related

How to make a button respond to touch event even if the touch's location is not contained in the button's rect ?

TODO:
I want make a button respond to all the touch event located in the superview, which contains a textfield and a button.
HOW TO:
I override the superView 's method hitTest:withEvent:, the superView which is a custom view .
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
BOOL isContained = [self pointInside:point withEvent:event];
if (self.hidden || self.alpha <= 0.1 || self.userInteractionEnabled == NO || !isContained)return nil;
return self.button;
}
beside that,I also do these:
I set the button 's target-action only forEvent: UITouchUpInside,the setup is
not working better。 the button can receive the touch event outside it,and always can be highlighten ,but sometime can calls the action when the touch point locate near outside the button,sometimes can't.
when I set the target-action method for Event:UITouchUpInside | |UITouchupOutside ,that works.
Question: Can someone explain this ?
I have complete Xcode project at the site:https://github.com/hansonboy/TestHit
maybe you can download ,and run it, and find the reason. thanks.
comments:I use Interface Builder to do these. Xcode 7.3
UIButton tracks some of the touch events(tracking==YES) but doesn't treat them as ones, that happen inside the control’s bounds(touchInside==NO). Just check the UIButton tracking, touchInside values in the endTrackingWithTouch:withEvent method.
Means, hit-testing mechanism works as expected: UIButton handles the event but doesn't trigger an action for the TouchUpInside control's event. And it is up to UIButton object to decide whether or not to generate this event. I found out that when a touch is pretty close to the superviews bounds, the UIButton's pointInside:withEvent: gets called. In your case it returns NO and the action is not triggered in the end.
As a solution I'd define a category or subclass UIButton in the next way:
#interface Button : UIButton
#end
#implementation Button
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.enabled || self.hidden || !self.userInteractionEnabled || self.alpha == 0) return [super pointInside:point withEvent:event];
CGPoint pnt = [self convertPoint:point toView:self.superview];
return CGRectContainsPoint(self.superview.bounds, pnt);
}
#end

How to determine "TapEnded" in iOS

I want to change background color of UIView tap, I know how to catch touch events and gestures on UIView in iOS.
I can change color when touchesBegan fires and then change it to original color when touchesEnded or touchesCancelled fires.
This works fine, until user tap on UIView really fast, this can be determined by UITapGestureRecognizer and change background color, but I don't know how to change color after that!
I don't want to use some kind of "Timer" or similar approach.
any suggestion?
You can detect that the tap ended by checking its state:
Objective-C
if (recognizer.state == UIGestureRecognizerStateEnded) {
// change the color here
}
Swift
if recognizer.state == .Ended {
// change the color here
}
From your question it seems that you have successfully done it with following delegates of UIView.
touchesBegan:
touchesEnded:
touchesCancelled:
The second option is to use UITapGestureRecognizer which was already mentioned by you. Please try using following code in your gesture recognizer method.
Add gesture to your view:
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(viewTap:)];
[yourView addGestureRecognizer:tapGesture ];
Your gesture recognizer method
-(void) viewTap:(UITapGestureRecognizer *) gestureRecognizer
{
if(gestureRecognizer.state == UIGestureRecognizerStateEnded)
{
//All fingers are lifted.
}
}

How to disable multi touch on UIPangestureRecognizer?

I have a UICollectionView to which I have added UILongPressGestureRecognizer and UIPangestureRecognizer for long press and reordering cells. And also I have added UIPanGestureRecognizer for all the UICollectionViewCells to show delete and few options on the right side.
My problem is when I pan two UICollectionViewCells with two fingers, both the UICollectionViewCells are detecting the pan and showing the options.
I want only one of the UICollectionViewCell to detect its UIPangestureRecognizer at a time. Is there any solution?.
Can anyone please help me out on this?.
Thanks in advance.
Have you tried the minimumNumberOfTouches / maximumNumberOfTouches property of UIPangestureRecognizer?
You can disable multi touch on collection it self. by making simple property.
[UICollectionView setMultipleTouchEnabled:NO];
If still problem is not being solve due to Gesture view implementation then you can use TouchedFlag for maintain touch on cell.
You can set in
- (IBAction) panGesture:(UIPanGestureRecognizer *)gesture;
You can set TouchedFlag to 1 while
if (gesture.state == UIGestureRecognizerStateBegan)
{
TouchedFlag=1;
}
And set back while PanGesture get ended in
if (gesture.state == UIGestureRecognizerStateEnded)
{
TouchedFlag=0;
}
So your finale code should look like
- (IBAction) panGesture:(UIPanGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateBegan && TouchedFlag==0)
{
TouchedFlag=1;
//Do your PAN openration
}
else if (gesture.state == UIGestureRecognizerStateBegan && TouchedFlag==1) {
//just prompt msg to user then single view at a time allowd to PAN
}
else if (gesture.state == UIGestureRecognizerStateEnded) {
TouchedFlag=0;
}
}

How to detect the touch point of iCarouselButtonDemo?

I am using iCarouselButtonDemo to create an arc button menu. I want to disable the scrolling when user touch the space other than the buttons. But now we can scroll the view by touching every point of the UIView. How can I detect the touch point of the view and disable the scrolling when user touch the outside of the 5 buttons
This is my view. This is scrolling when I touch even the bottom of the view. How can I stop it?
Thanks
In the iCarousel implementation file, you will add the following code in gestureRecognizerShouldBegin method. So it looks like this. It firstly get the touch point in the iCarousel view, and find the inner most view responding to the touch through hitTest. If the view is not a button, you stop the pan gesture.
if ([gesture isKindOfClass:[UIPanGestureRecognizer class]])
{
CGPoint point = [gesture locationInView:self];
UIView *touchedView = [self hitTest:point withEvent:nil];
if (![touchedView isKindOfClass:[UIButton class]]) {
return NO;
}
//ignore vertical swipes

Allow UIScrollView and its subviews to both respond to a touch

I want both my UIScrollView and its subviews to receive all touch events inside the subview. Each can respond in its own way.
Alternatively, if tap gestures were forwarded to subviews, all would be well.
A lot of people are struggling in this general area. Here are a few of the many related questions:
How does UIScrollView steal touches from its subviews
How to steal touches from UIScrollView?
How to Cancel Scrolling in UIScrollView
Incidentally, if I override hitTest:withEvent: in the scroll view, I do see the touches as long as userInteractionEnabled is YES. But that doesn't really solve my problem, because:
1) At that point, I don't know if it's a tap or not.
2) Sometimes I need to set userInteractionEnabled to NO.
EDIT: To clarify, yes, I want to treat taps differently from pans. Taps should be handled by subviews. Pans can be handled by the scroll view in the usual way.
First, a disclaimer. If you set userInteractionEnabled to NO on the UIScrollView, no touch events will be passed to the subviews. So far as I'm aware, there's no way around that with one exception: intercept touch events on the superview of the UIScrollView, and specifically pass those events to the subviews of UIScrollView. To be honest, though, I don't know why you would want to do this. If you're wanting to disable specific UIScrollView functionality (like...well, scrolling) you can do that easily enough without disabling UserInteraction.
If I understand your question, you need tap events to be processed by the UIScrollView and passed to the subviews? In any case (whatever the gesture is), I think what you're looking for is the protocol method gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: in the protocol UIGestureRecognizerDelegate. In your subviews, whatever gesture recognizers you have, set a delegate (probably whatever class is setting the UIGestureReconginzer in the first place) on the gesture recognizer. Override the above method and return YES. Now, this gesture will be recognized along with any other recognizers that might have 'stolen' the gesture (in your case, a tap). Using this method you can even fine tune your code to only send certain kinds of gestures to the subviews or send the gesture only in certain situations. It gives you a lot of control. Just be sure to read about the method, especially this part:
This method is called when recognition of a gesture by
either gestureRecognizer or otherGestureRecognizer would block the
other gesture recognizer from recognizing its gesture. Note that
returning YES is guaranteed to allow simultaneous recognition;
returning NO, on the other hand, is not guaranteed to prevent
simultaneous recognition because the other gesture recognizer's
delegate may return YES.
Of course, there's a caveat: This only applies to gesture recognizers. So you may still have problems if you're trying to use touchesBegan:, touchesEnded, etc to process the touches. You can, of course, use hitTest: to send raw touch events on to the subviews, but why? Why process the events using those methods in UIView, when you can attach a UIGestureRecognizer to a view and get all of that functionality for free? If you need touches processed in a way that no standard UIGestureRecognizer can provide, subclass UIGestureRecognizer and process the touches there. That way you get all the the functionality of a UIGestureRecognizer along with your own custom touch processing. I really think Apple intended for UIGestureRecognizer to replace most (if not all) of the custom touch processing code that developers use on UIView. It allows for code-reuse and it's a lot easier to deal with when mitigating what code processes what touch event.
I don't know if this can help you, but I had a similar problem, where I wanted the scrollview to handle double-tap, but forward single tap to subviews. Here is the code used in a CustomScrollView
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
// Coordinates
CGPoint point = [touch locationInView:[self.subviews objectAtIndex:0]];
// One tap, forward
if(touch.tapCount == 1){
// for each subview
for(UIView* overlayView in self.subviews){
// Forward to my subclasss only
if([overlayView isKindOfClass:[OverlayView class]]){
// translate coordinate
CGPoint newPoint = [touch locationInView:overlayView];
//NSLog(#"%#",NSStringFromCGPoint(newPoint));
BOOL isInside = [overlayView pointInside:newPoint withEvent:event];
//if subview is hit
if(isInside){
Forwarding
[overlayView touchesEnded:touches withEvent:event];
break;
}
}
}
}
// double tap : handle zoom
else if(touch.tapCount == 2){
if(self.zoomScale == self.maximumZoomScale){
[self setZoomScale:[self minimumZoomScale] animated:YES];
} else {
CGRect zoomRect = [self zoomRectForScrollView:self withScale:self.maximumZoomScale withCenter:point];
[self zoomToRect:zoomRect animated:YES];
}
[self setNeedsDisplay];
}
}
Of course, the effective code should be changed, but at this point you should have all the informations you need to decide if you have to forward the event. You might need to implement this in another method as touchesMoved:withEvent:.
Hope this can help.
I was having this same problem, but with a scrollview that was inside UIPageViewController, so it had to be handled slightly differently.
By changing the cancelsTouchesInView property to false for each recognizer on the UIScrollView I was able to receives touches to buttons inside the UIPageViewController.
I did so by adding this code into viewDidLoad:
guard let recognizers = self.pageViewController.view.subviews[0].gestureRecognizers else {
print("No gesture recognizers on scrollview.")
return
}
for recognizer in recognizers {
recognizer.cancelsTouchesInView = false
}
If what you need is to differ between a touch and a scroll then you can test if touches has been moved. If this is a tap then touchHasBeenMoved will not be called then you can assume this is a touch.
At this point you can set a boolean to indicate if a movnent accoured and set this Boolean as a condition in your other methods.
I am on the road but if that's what you need I will be able to explain better later.
A hackish way to achieve your objective - not 100% exact - is to subclass the UIWindow and override the - (void)sendEvent:(UIEvent *)event;
A quick example:
in SecondResponderWindow.h header
//SecondResponderWindow.h
#protocol SecondResponderWindowDelegate
- (void)userTouchBegan:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchMoved:(id)tapPoint onView:(UIView*)aView;
- (void)userTouchEnded:(id)tapPoint onView:(UIView*)aView;
#end
#interface SecondResponderWindow : UIWindow
#property (nonatomic, retain) UIView *viewToObserve;
#property (nonatomic, assign) id <SecondResponderWindowDelegate> controllerThatObserves;
#end
in SecondResponderWindow.m
//SecondResponderWindow.m
- (void)forwardTouchBegan:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchBegan:touch onView:aView];
}
- (void)forwardTouchMoved:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchMoved:touch onView:aView];
}
- (void)forwardTouchEnded:(id)touch onView:(UIView*)aView {
[controllerThatObserves userTouchEnded:touch onView:aView];
}
- (void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
if (viewToObserve == nil || controllerThatObserves == nil) return;
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
if ([touch.view isDescendantOfView:viewToObserve] == NO) return;
CGPoint tapPoint = [touch locationInView:viewToObserve];
NSValue *pointValue = [NSValue valueWithCGPoint:tapPoint];
if (touch.phase == UITouchPhaseBegan)
[self forwardTouchBegan:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseMoved)
[self forwardTouchMoved:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseEnded)
[self forwardTouchEnded:pointValue onView:touch.view];
else if (touch.phase == UITouchPhaseCancelled)
[self forwardTouchEnded:pointValue onView:touch.view];
}
It's not 100% conforms to what your were expecting - because your second responder view does not handle the touch event natively via -touchDidBegin: or so, and has to implement the SecondResponderWindowDelegate. However this hack does allow you to handle touch events on additional responders.
This method is inspired by and extended from MITHIN KUMAR's TapDetectingWindow

Resources