Search code examples
iosswiftcore-animationcalayercabasicanimation

CABasicAnimation runs after Implicit Animation


I want a layer to behave like this:

correct

Instead, it behaves like this:

improper

The card flip animation is created by two CABasicAnimations applied in a CAAnimationGroup. The incorrect spin effect happens because the implicit animation from the CALayer property change runs first and then my animation specified in the CABasicAnimation runs. How can I stop the implicit animation from running so that only my specified animation runs?

Here's the relevant code:

class ViewController: UIViewController {

  var simpleLayer = CALayer()

  override func viewDidLoad() {
    super.viewDidLoad()

    let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
    self.view.addGestureRecognizer(tap)

    simpleLayer.frame = CGRect(origin: CGPoint(x: view.bounds.width / 2 - 50, y: view.bounds.height / 2 - 50), size: CGSize(width: 100, height: 100))
    simpleLayer.backgroundColor = UIColor.blackColor().CGColor
    view.layer.addSublayer(simpleLayer)
  }

  func handleTap() {
    let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
    xRotation.toValue = 0
    xRotation.byValue = M_PI

    let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
    yRotation.toValue = 0
    yRotation.byValue = M_PI

    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")

    let group = CAAnimationGroup()
    group.animations = [xRotation, yRotation]
    group.duration = 0.6
    group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    simpleLayer.addAnimation(group, forKey: nil)
  }
}

Solution

  • @LucasTizma had the correct answer.

    Surround your animation with CATransaction.begin(); CATransaction.setDisableActions(true) and CATransaction.commit(). This will disable the implicit animation and make the CAAnimationGroup animate correctly.

    Here's the final result:

    triangle flip animation

    This is the important snippet of code in Swift 3:

    CATransaction.begin()
    CATransaction.setDisableActions(true)
    
    let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
    xRotation.toValue = 0
    xRotation.byValue = M_PI
    
    let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
    yRotation.toValue = 0
    yRotation.byValue = M_PI
    
    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
    simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
    
    let group = CAAnimationGroup()
    group.animations = [xRotation, yRotation]
    simpleLayer.add(group, forKey: nil)
    
    CATransaction.commit()
    

    And this is the full code for the depicted animation with an iOS app:

    class ViewController: UIViewController {
    
      var simpleLayer = CALayer()
    
      override func viewDidLoad() {
        super.viewDidLoad()
    
        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap))
        self.view.addGestureRecognizer(tap)
    
        let ratio: CGFloat = 1 / 5
        let viewWidth = view.bounds.width
        let viewHeight = view.bounds.height
        let layerWidth = viewWidth * ratio
        let layerHeight = viewHeight * ratio
    
        let rect = CGRect(origin: CGPoint(x: viewWidth / 2 - layerWidth / 2,
                                          y: viewHeight / 2 - layerHeight / 2),
                          size: CGSize(width: layerWidth, height: layerHeight))
    
        let topRightPoint = CGPoint(x: rect.width, y: 0)
        let bottomRightPoint = CGPoint(x: rect.width, y: rect.height)
        let topLeftPoint = CGPoint(x: 0, y: 0)
    
        let linePath = UIBezierPath()
    
        linePath.move(to: topLeftPoint)
        linePath.addLine(to: topRightPoint)
        linePath.addLine(to: bottomRightPoint)
        linePath.addLine(to: topLeftPoint)
    
        let maskLayer = CAShapeLayer()
        maskLayer.path = linePath.cgPath
    
        simpleLayer.frame = rect
        simpleLayer.backgroundColor = UIColor.black.cgColor
        simpleLayer.mask = maskLayer
    
        // Smooth antialiasing
        // * Convert the layer to a simple bitmap that's stored in memory
        // * Saves CPU cycles during complex animations
        // * Rasterization is set to happen during the animation and is disabled afterwards
        simpleLayer.rasterizationScale = UIScreen.main.scale
    
        view.layer.addSublayer(simpleLayer)
      }
    
      func handleTap() {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        CATransaction.setCompletionBlock({
          self.simpleLayer.shouldRasterize = false
        })
    
        simpleLayer.shouldRasterize = true
    
        let xRotation = CABasicAnimation(keyPath: "transform.rotation.x")
        xRotation.toValue = 0
        xRotation.byValue = M_PI
    
        let yRotation = CABasicAnimation(keyPath: "transform.rotation.y")
        yRotation.toValue = 0
        yRotation.byValue = M_PI
    
        simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.x")
        simpleLayer.setValue(M_PI, forKeyPath: "transform.rotation.y")
    
        let group = CAAnimationGroup()
        group.animations = [xRotation, yRotation]
        group.duration = 1.2
        group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        simpleLayer.add(group, forKey: nil)
    
        CATransaction.commit()
      }
    }