Search code examples
iosanimationcore-animationcashapelayer

CAShapeLayer strange animation behavior


I am trying to create an animation where two line are faded out when the user drags a UIView and faded back in when user releases dragging.
Therefore I have two functions undrawLines (called at pan gesture start) and redrawLines (called at pan gesture end) which are called by my UIPanGestureRecognizer action handler.

func undrawLines() {

    line1.opacity = 0.0
    line2.opacity = 0.0

    line1.removeAllAnimations()
    line2.removeAllAnimations()

    let opacityLine = CABasicAnimation(keyPath: "opacity")
    opacityLine.fromValue = 1.0
    opacityLine.toValue = 0.0
    opacityLine.duration = 0.15

    line1.add(opacityLine, forKey: "disappearLine1")
    line2.add(opacityLine, forKey: "disappearLine2")

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
        mill.line1.removeFromSuperlayer()
        mill.line2.removeFromSuperlayer()
    })
}

func redrawLines() {

    line1.opacity = 1.0
    line2.opacity = 1.0

    print("redraw")
    line1.removeAllAnimations()
    line2.removeAllAnimations()

    self.layer.addSublayer(line1)
    self.layer.addSublayer(line2)

    let opacityLine = CABasicAnimation(keyPath: "opacity")
    opacityLine.fromValue = 0.0
    opacityLine.toValue = 1.0
    opacityLine.duration = 0.15

    line1.add(opacityMill, forKey: "appearLine1")
    line2.add(opacityMill, forKey: "appearLine2")
}

The problem is that when redrawLines gets called while the undrawLines animation is still running, the lines show a strange behavior and opacity is 0.
Here is a demo, the first part shows how it should be, the second one shows the bug:

enter image description here


Solution

  • I believe your issue here is a race condition with your completion handler:

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: {
        mill.line1.removeFromSuperlayer()
        mill.line2.removeFromSuperlayer()
    })
    

    If your user releases, and therefore redrawLines gets called before the 0.3 second timeout, this still gets called and removes the lines.

    You probably want to keep a state flag that indicates the current intent and then check it in the asynchronous callback:

    func undrawLines() {
    
        self.linesHidden = true // update state
    
        line1.opacity = 0.0
        line2.opacity = 0.0
    
        line1.removeAllAnimations()
        line2.removeAllAnimations()
    
        let opacityLine = CABasicAnimation(keyPath: "opacity")
        opacityLine.fromValue = 1.0
        opacityLine.toValue = 0.0
        opacityLine.duration = 0.15
    
        line1.add(opacityLine, forKey: "disappearLine1")
        line2.add(opacityLine, forKey: "disappearLine2")
    
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { [weak self] in
            if self?.linesHidden == true { // check this is still what we want to do
                mill.line1.removeFromSuperlayer()
                mill.line2.removeFromSuperlayer()
            }
        })
    }
    
    func redrawLines() {
    
        self.linesHidden = false // update state
    
        line1.opacity = 1.0
        line2.opacity = 1.0
    
        print("redraw")
        line1.removeAllAnimations()
        line2.removeAllAnimations()
    
        self.layer.addSublayer(line1)
        self.layer.addSublayer(line2)
    
        let opacityLine = CABasicAnimation(keyPath: "opacity")
        opacityLine.fromValue = 0.0
        opacityLine.toValue = 1.0
        opacityLine.duration = 0.15
    
        line1.add(opacityMill, forKey: "appearLine1")
        line2.add(opacityMill, forKey: "appearLine2")
    }
    

    You'll clearly need to add the instance var linesHidden to the class for this to work too :)