Malakim
Malakim

Reputation: 1343

Animation delay on left side of screen in iOS keyboard extension

I'm working on a keyboard extension for iOS. However, I'm having some weird issues with animations / layers not appearing instantly on the far left of the screen. I use layers / animations to show a "tool tip" when the user presses a key. For all keys except A and Q the tool tips are displayed instantly, but for these two keys there seems to be a slight delay before the layer and animation appears. This only happens on touch down, if I slide into the Q or A hit area the tool tips gets rendered instantly. My debugging shows that the code executes exactly the same for all keys, but for these two keys it has no immediate effect.

Any ideas on if there's anything special with the left edge of the screen that might cause this behaviour? Or am I doing something stupid that might be the cause of this?

This is part of my touch handling code that triggers the tool tip rendering:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    if(!shouldIgnoreTouches()) {
        for touch in touches {
            let location = (touch ).locationInView(self.inputView)

            // pass coordinates to offset service to find candidate keys
            let keyArray = keyOffsetService.getKeys(_keyboardLayout!, location: location)
            let primaryKey = keyArray[0]

            if primaryKey.alphaNumericKey != nil {
                let layers = findLayers(touch )

                if layers.keyLayer != nil {
                    graphicsService.animateKeyDown(layers.keyLayer as! CATextLayer, shieldLayer: layers.shieldLayer)
                    _shieldsUp.append((textLayer:layers.keyLayer, shieldLayer:layers.shieldLayer))
                }
            }
         }
    }
}

animation code:

func animateKeyDown(layer:CATextLayer, shieldLayer:CALayer?) {
    if let sLayer = shieldLayer {
        keyDownShields(layer, shieldLayer: sLayer)
        CATransaction.begin()
        CATransaction.setDisableActions(true)

        let fontSizeAnim = CABasicAnimation(keyPath: "fontSize")
        fontSizeAnim.removedOnCompletion = true
        fontSizeAnim.fromValue = layer.fontSize
        fontSizeAnim.toValue = layer.fontSize * 0.9
        layer.fontSize = layer.fontSize * 0.9

        let animation  = CABasicAnimation(keyPath: "opacity")
        animation.removedOnCompletion = true
        animation.fromValue = layer.opacity
        animation.toValue = 0.3
        layer.opacity = 0.3

        let animGroup = CAAnimationGroup()
        animGroup.animations = [fontSizeAnim, animation]
        animGroup.duration = 0.01
        layer.addAnimation(animGroup, forKey: "down")

        CATransaction.commit()
    }
}

unhide tooltip layer:

private func keyDownShields(layer:CATextLayer, shieldLayer:CALayer) {
    shieldLayer.hidden = false
    shieldLayer.setValue(true, forKey: "isUp")
    shieldLayer.zPosition = 1
    shieldLayer.removeAllAnimations()
    layer.setValue(true, forKey: "isUp")
}

Image of the keyboard with tooltip

Upvotes: 4

Views: 640

Answers (2)

nikans
nikans

Reputation: 2565

The official solution 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:).

Upvotes: 0

Randy
Randy

Reputation: 2358

This is caused by a feature in iOS 9 which allows the user to switch apps by force pressing the left edge of the screen while swiping right.

You can turn this off by disabling 3D touch but this is hardly a solution.

I am not aware of any API that allows you to override this behavior.

Upvotes: 2

Related Questions