swifteventsuikittouchuiresponder

Stop propagation of touch event to views behind in 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 ?


Solution

  • 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.