Ben Morrow Ben Morrow - 3 months ago 22
iOS Question

CABasicAnimation runs after Implicit Animation

I want a layer to behave like this:

correct

Instead, it behaves like this:

improper

I think that the implicit animation from the CALayer property change runs first and then my animation specified in the CABasicAnimation runs. Why is the implicit animation happening at all? I've used code snippets like this in the past with only the CABasicAnimation, not the implicit animation.

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

}

Answer

@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

And the final code in Swift 3 for iOS:

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
    view.layer.addSublayer(simpleLayer)
  }

  func handleTap() {
    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]
    group.duration = 0.6
    group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    simpleLayer.add(group, forKey: nil)

    CATransaction.commit()
  }

}