Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Touch events are delayed near left screen edge on iOS 9 only. How to fix it?

I am developing a keybaord extension for iOS. On iOS 9 the keys react imediatelly except for keys along left edge of the keyboard. Those react with around 0.2 second delay. The reason is that the touches are simply delivered with this delay to the UIView that is root view of my keyboard. On iOS 8 there is no such delay.

My guess is that this delay is cause by some logic that is supposed to recognize gesture for opening "running apps screen". That is fine but the delay on a keyboard is unacceptable. Is there any way how to get those events without delay? Perhaps just setting delaysTouchesBegan to false on some UIGestureRecognizer?

like image 373
Rasto Avatar asked Nov 09 '15 22:11

Rasto


3 Answers

This is for anyone using later versions of iOS (this is working on iOS 9 and 10 for me). My issue was caused by the swipe to go back gesture interfering with my touchesBegan method by preventing it from firing on the very left edge of the screen until either the touch was ended, or the system recognised the movement to not be that of the swipe to go back gesture.

In your viewDidLoad function in your controller, simply put:

self.navigationController?.interactivePopGestureRecognizer?.delaysTouchesBegan = false
like image 145
sutherlandahoy Avatar answered Nov 19 '22 21:11

sutherlandahoy


The official solution since iOS11 is overriding preferredScreenEdgesDeferringSystemGestures of your UIInputViewController.

https://developer.apple.com/documentation/uikit/uiviewcontroller/2887512-preferredscreenedgesdeferringsys

However, it doesn't seem to work on iOS 13 at least. As far as I understand, that happens due to preferredScreenEdgesDeferringSystemGestures not working properly when overridden inside UIInputViewController, at least on iOS 13.

When you override this property in a regular view controller, it works as expected:

override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
    return [.left, .bottom, .right]
}

That' not the case for UIInputViewController, though.

UPD: It appears, gesture recognizers will still get .began state update, without the delay. So, instead of following the rather messy solution below, you can add a custom gesture recognizer to handle touch events.

You can quickly test this adding UILongPressGestureRecognizer with minimumPressDuration = 0 to your control view.

Another solution:

My original workaround was calling touch down effects inside hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?, which is called even when the touches are delayed for the view.

You have to ignore the "real" touch down event, when it fires about 0.4s later or simultaneously with touch up inside event. Also, it's probably better to apply this hack only in case the tested point is inside ~20pt lateral margins.

So for example, for a view with equal to screen width, the implementation may look like:

let edgeProtectedZoneWidth: CGFloat = 20

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    let result = super.hitTest(point, with: event)

    guard result == self else {
        return result
    }

    if point.x < edgeProtectedZoneWidth || point.x > bounds.width-edgeProtectedZoneWidth
    {
        if !alreadyTriggeredFocus {
            isHighlighted = true
        }
        triggerFocus()
    }

    return result
}

private var alreadyTriggeredFocus: Bool = false

@objc override func triggerFocus() {
    guard !alreadyTriggeredFocus else { return }
    super.triggerFocus()
    alreadyTriggeredFocus = true
}

override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesCancelled(touches, with: event)

    alreadyTriggeredFocus = false
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    super.touchesEnded(touches, with: event)

    alreadyTriggeredFocus = false
}

...where triggerFocus() is the method you call on touch down event. Alternatively, you may override touchesBegan(_:with:).

like image 25
nikans Avatar answered Nov 19 '22 21:11

nikans


If you have access to the view's window property, you can access these system gesture recognizers and set delaysTouchesBegan to false.

Here's a sample code in swift that does that

if let window = view.window,
   let recognizers = window.gestureRecognizers {
   recognizers.forEach { r in
        // add condition here to only affect recognizers that you need to
        r.delaysTouchesBegan = false
   }
}

Also relevant: UISystemGateGestureRecognizer and delayed taps near bottom of screen

like image 1
jlajlar Avatar answered Nov 19 '22 23:11

jlajlar