Search code examples
iosswiftcore-animationcashapelayercabasicanimation

CAShapeLayer detect touch during animation Swift


I can detect a touch of a CAShapeLayer like this (touchesEnded):

let touchLocation : CGPoint = (touch as! UITouch).locationInView(self.view)

for shape in shapes{
    if CGPathContainsPoint(shape.path, nil, touchLocation, false){
        print("Layer touch")
    }
}

And I can animate the path of a CAShapeLayer like this:

let newShapePath = UIBezierPath(arcCenter: toPoint, radius: 20, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true).CGPath

// animate the `path`
let animation = CABasicAnimation(keyPath: "path")
animation.toValue = newShapePath
animation.duration = CFTimeInterval(duration)

animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animation.fillMode = kCAFillModeBoth
animation.removedOnCompletion = false

shape.addAnimation(animation, forKey: animation.keyPath)

But while the animation is happening, touches aren't detected on the CAShapeLayer. Is it possible to detect a touch on the a CAShapeLayer while animating the path?


Solution

  • You can access the layer's presentationLayer in order to do this. This will provide you with a rough approximation to the 'in flight' values of a given layer while animating. For example:

    for shape in shapes {
    
        // gets the layer's presentation layer if it exists – else fallback on the model layer
        let presentationLayer = shape.presentationLayer() as? CAShapeLayer ?? shape
    
        if CGPathContainsPoint(presentationLayer.path, nil, touchLocation, false){
            print("Layer touch")
        }
    }
    

    Also, as a side note, it's generally considered bad practice to use removedOnCompletion = false if you're not using the animation delegate. Instead of leaving the animation lingering, you should just update the layer's model values to represent its new state. You can do this through a CATransaction to ensure that no implicit animations are generated. For example:

    let animation = CABasicAnimation(keyPath: "path")
    animation.fromValue = shape.path
    animation.toValue = newShapePath
    animation.duration = CFTimeInterval(duration)
    
    animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
    
    shape.addAnimation(animation, forKey: animation.keyPath)
    
    // update the layer's model values
    CATransaction.begin()
    CATransaction.setDisableActions(true)
    shape.path = newShapePath
    CATransaction.commit()