Stop propagation of touch event to views behind in Swift - swift

I have a scrollview and a subview inside it. If the user does a certain touch in that subview, I want to prevent the scrollview to move, in order to let the user do their stuff in the subview without being bothered by the scrollview movement. If the certain touch is not detected, then the scrollview should move normally .
I intercept these gestures with raw touch events touchesBegan, touchesMoved. I don't use gestureRecognizers because the gestures I want to recognise are very specifics and I feel more confortable using no abstraction layer to recognise them.
I know after seeing many answers on SO, I could just hold a reference of the scrollview behind and stop its movement if I detect the gesture. I'm looking for a more stable solution. I want to stop the propagation of the event (to any view behind), if I detect the gesture, without having to hold the references of any of these views behind.
As I understood, view in iOS are subclasses of UIResponder. When UIKit detects a touch on the screen, it gives the event to the first responder, which is generally the top most subview. My question is : in touchesBegan how to tell UIKit : "Do not send the event to any other following view in the responder chain". If I can see the scrollview moving behind, UIKit must have forwarded the event to it (despite I'm not calling super in touchesBegan)
In Android for example, onTouchEvent function of View class returns a bool. false tells android to continue propagating the event, true tells to stop propagating. I'm looking for the same mechanism in iOS:
#Override
public boolean onTouchEvent(MotionEvent e)
{
return true ; // stops propagation
}
In Javascript (jQuery), there's quite the same mechanism :
$('#myview').bind('click', function(e) {
e.stopPropagation()
})
How to do that in Swift ?

Found the solution. This was tricky to understand so I'll try to make it easy if anyone has the same problem :
As stated in another SO answer, the "raw touch system" ( touchesBegen, touchesMoved ..) and the "gesture recognizer system" are mutually exclusive, both of them are actually at the "raw" view level, and there are independent.
This means that when you have a view and you touch it, you have a chance that your touch is handled by the gestureRecognizer system instead of the raw touch system. overriding next UIResponder property by override var next:UIResponder? { get { return nil }} only force UIKit not to forward the event in the raw touch system, the gesture recognition of views behind are still fired, because it's a system completely apart.
In my case, I tried to override var next:UIResponder? { get { return nil }} : the touchesBegan of the views behind remained quiet as expected, but I could still recognize gesture there.
So, it appears that UIScrollView uses gestureRecognizer to handle user touches. The solution is to shut down the gesture recognizer system from your top most view so the gesture is not forwarded : this can be done using :
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool
{
return !yourConditionToShutDownGR
}
I was confused because the Android gesture "detector" system is built on top of the raw touch system. When you catch a touch event in public boolean onTouchEvent(MotionEvent event) you can pass it as a parameter of a gestureDetector, which returns if it found a gesture or not. That's not the same approach for iOS in which the gestureRecognizer system is built aside of the raw touch system.

Related

How to suspend touchesBegan and only use touchesMoved vice versa?

At the moment I am programming a game in swift, Xcode and I would like the user to be able to choose what controls they want (either slide or tap). As of now I have been able to make my SKNode slide in the main game scene, as well as respond to tapping. However I have found that the SKNode shakes constantly when you slide (as the touchesBegan function thinks I'm tapping on the screen).
If anyone knows the best way to suspend one of the functions while the other stays active please let me know :)
For example:
The user chooses to play the game by sliding, I would like to suspend the touchesBegan function but keep the touchesMoved active.
There is a delegate method to check if one gesture fails, if is true the other gesture is recognized. For example, in my project I have two gestures, tap once and tap twice and I used:
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer.numberOfTouches == 2 {
return true
}
return false
}
For more information check Apple's reference:
https://developer.apple.com/documentation/uikit/uigesturerecognizer/1620006-shouldrequirefailure

How to prioritize gesture recognizers and touches in a UIView

From reading the UIGestureRecognizer Class Reference it is implied that the API will handle the prioritizing of touches and gesture controls for you, making sure that your touchesBegan and related methods are not called on the view unless the gesture recognizers have first failed:
A window delivers touch events to a gesture recognizer before it delivers them to the hit-tested view attached to the gesture recognizer. Generally, if a gesture recognizer analyzes the stream of touches in a multi-touch sequence and does not recognize its gesture, the view receives the full complement of touches. If a gesture recognizer recognizes its gesture, the remaining touches for the view are cancelled.
I have added a swipe gesture to my view, and it is working. Via some logging, when I do a single swipe, the method reports as such. However, my touchesBegan method is also reporting via its log, despite that the touchesCancelled method is, as expected, also receiving a message.
I want, and expect, the gesture recognize to prevent touchesBegan or touchesMoved from being called.
So my question is: for the gesture recognizer to in fact delay touches based on its state, is there additional setup necessary? The docs do not suggest anything else as necessary.
My setup is simply:
swipeUpTwoFinger=[[[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(doubleSwipeUp:)]autorelease];
swipeUpTwoFinger.direction=UISwipeGestureRecognizerDirectionUp;
swipeUpTwoFinger.numberOfTouchesRequired=2;
[self addGestureRecognizer:swipeUpTwoFinger];
I have also tried this test to make sure a recognizer has failed before processing with touchesBegan (this test should not be necessary if you believe what the docs say above) but the touchesBegan is still processing the log line after this test:
if (swipeUpTwoFinger.state==UIGestureRecognizerStateFailed)
It sounds like you need:
swipeUpTwoFinger.delaysTouchesBegan = YES;

Receive touchesMoved when touch began in the super view

In iOS, I would like to receive touchesMoved events in a subview even when the touch began i the super view! Ideally, I would also like to receive touchesBegan when the moving finger reaches the subview.
What is the best way to achieve this?
I guess I could override touchesMoved:withEvent: in the superview, do a hit-test and then call the subview, but is there a better solution? (I have tried to resign first responder in the superview but that did not help)
I had to solve this exact same problem today, and I just did.
Say, you have the following -- this is an abstract description of exactly my case:
Main view containing a button and a sub view
When touches begin on the main view you want to receive the touch events on the sub view
One exception is that button: if you tap the button in the main view, then the button should handle that
The solution is to override the -hitTest:withEvent: method. The purpose of that method is to return the view that should handle the touches.
You have two options: override it in the sub view or override it in the main view. If you want ALL touches to be handled by the sub view, you can just have the -hitTest:withEvent on the sub view return self and be done with it. In my case I still needed the button on the main view to have its own touch detection, so my fix was to override the -hitTest:withEvent: method on the main view instead, as follows:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(self.myButton.frame, point))
{
return self.myButton;
}
else
{
return self.mySubView;
}
}
This works like an absolute charm! Hope this works for you as well :-)
You should of course modify the details inside -hitTest:withEvent: to suit your specific needs -- maybe you want to limit this override to a specific region or a specific set of controls -- but the point is that the answer to the main question on this thread lies in the override of this -hitTest:withEvent: method.
Erik

Detecting a finger being held on an object

I am trying to have an image that when the user touches it, it wiggles and as soon as the user lifts their finger it stops.
Is there a gesture that I can use to detect when the finger is down, not just on the initial touch, or when the user moves there finger?
I have tried a LongPress gesture, but that does not get called the entire time the finger is on the view. Can anyone help me with the best way to active this. Right now i am doing it using touchesBegin, touchesMoved, touchesEnd, but i was wondering if there is a better way.
Any suggestions are greatly appreciated.
Thanks
EDIT
Based on the comments, I slightly misunderstood the original question, so I edit my answer to a different solution, which hopefully is a bit more clear (and answers the actual question - not the one that was in my head).
A LongPress gesture is continuous (where a tap gesture is not). That means, the recognizer callback will continue to be invoked until the gesture is complete - which does not happen until the "longpress" is released. So, the following should do what you want. NOTE: I think you want to "start shaking" a view when the long-press is recognized, then "stop shaking" the view when the fingers are released. I just pretended you have functions for that. Substitute appropriately.
- (void)handleLongPress:(UILongPressGestureRecognizer*)gestureRecognizer
{
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
StartShakingView(gestureRecognizer.view);
} else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
StopShakingView(gestureRecognizer.view);
}
}
The Apple Touches sample includes code that demonstrate using both UIResponder and UIGestureRecognizer methods.
Either should work for what you're doing.
Simple answer - you could make the image a UIButton, and start the wiggle on TouchDown, and stop it on TouchUpInside or TouchUpOutside
It sounds like you want to subclass UIGestureRecognizer, which, as I recall, gets the touchesBegan:... and associated methods. Read the notes on subclassing in the UIGestureRecognizer reference. Or use a UIButton as SomaMan suggests.

When touches Cancelled method get invoked in iPhone?

I am able to understand that when user just touches the view, touches Began and Ended called. When user swipes their hand on a view, touches Moved method gets called. But when does touches Cancelled get called or by what action on user this method gets called?
I think probably the most common reason for touchesCancelled being called (since iOS 3.2 anyway) is following the recognition of a gesture by a UIGestureRecognizer. If your view has any kind of gesture recognizer attached to it then it is often very important to provide a custom implementation of the touchesCancelled method - note this includes ready made views that use gesture recognizers, including UIScrollView.
By default, gesture recognizers cancel the delivery of touches to the hit-test view upon recognition, although this behaviour can be disabled. This involves sending the touchesCancelled message to that view, most likely following a touchesBegan or touchesMoved message. If your touch handling code is relying on code implemented in the touchesEnded method, it is possible this may never be fired and some kind of serious problem could occur, hence the need to properly tie up any loose ends in touchesCancelled.
The ins and outs of gesture recognizer functionality is obviously a bit more complex than I've mentioned here - I would thoroughly recommend reading Apple's Gesture Recognizers documentation.
Also, check out the WWDC videos on gesture recognizers (starting from 2010).
Note: touches also get cancelled if you start a UIView animation after touchesBegan. To prevent this make sure you include UIViewAnimationOptionAllowUserInteraction:
e.g.
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionAllowUserInteraction animations:^{
self.aView.hidden = NO;
self.aView.alpha = 1;
} completion:nil];
From the Apple Reference documents
Sent to the receiver when a system
event (such as a low-memory warning)
cancels a touch event.
Discussion
This method is invoked when the Cocoa
Touch framework receives a system
interruption requiring cancellation of
the touch event; for this, it
generates a UITouch object with a
phase of UITouchPhaseCancel. The
interruption is something that might
cause the application to be no longer
active or the view to be removed from
the window
When an object receives a
touchesCancelled:withEvent: message it
should clean up any state information
that was established in its
touchesBegan:withEvent:
implementation.
The default implementation of this
method does nothing. However immediate
UIKit subclasses of UIResponder,
particularly UIView, forward the
message up the responder chain.
And, from the Event Handling Guide for iOS, p. 19:
It sends the touchesCancelled:withEvent: message when the touch sequence is cancelled by a system event, such as an incoming phone call.
I was handling touchesBegan()/touchesMoved() on a view under UIScrollView, which is challenging. My touches kept cancelled by somewhere when I pinch (somehow it is OK with single touch movement), I was investigating how to stop being cancelled. I figured out, that there is a property Can Cancel On Scroll on UIScrollView, and you may check it off to stop being cancelled, if your case is similar to my case.
It sounds there are many cases where your touches are being cancelled, so my answer is just one of them.