Codey
Codey

Reputation: 1233

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

Upvotes: 0

Views: 111

Answers (1)

George Green
George Green

Reputation: 4905

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 :)

Upvotes: 1

Related Questions