I have a navigation controller with two VCs. One VC is pushed onto the NavController, the other is presented on top of the NavController. The presented VC has a relatively complex animation involving a CAEmitter -> Animate birth rate down -> Fade out -> Remove. The pushed VC has an 'inputAccessoryView' and can become first responder.
The expected behavior is open presented VC -> Emitter Emits pretty pictures -> emitter stops gracefully.
The animation works perfectly. UNLESS I open pushed VC -> Leave -> go to presented VC. In this case when I open the presented VC the emitter emits pretty pictures -> they never stop. (Please do not ask me how long it took to figure this much out 🤬😔)
The animation code in question is:
let animation = CAKeyframeAnimation(keyPath: #keyPath(CAEmitterLayer.birthRate))
animation.duration = 1
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
animation.values = [1, 0 , 0]
animation.keyTimes = [0, 0.5, 1]
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
emitter.beginTime = CACurrentMediaTime()
let now = Date()
CATransaction.begin()
CATransaction.setCompletionBlock { [weak self] in
print("fade beginning -- delta: \(Date().timeIntervalSince(now))")
let transition = CATransition()
transition.delegate = self
transition.type = .fade
transition.duration = 1
transition.timingFunction = CAMediaTimingFunction(name: .easeOut)
transition.setValue(emitter, forKey: kKey)
transition.isRemovedOnCompletion = false
emitter.add(transition, forKey: nil)
emitter.opacity = 0
}
emitter.add(animation, forKey: nil)
CATransaction.commit()
The delegate method is:
extension PresentedVC: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if let emitter = anim.value(forKey: kKey) as? CALayer {
emitter.removeAllAnimations()
emitter.removeFromSuperlayer()
} else {
}
}
}
Here is the pushed VC:
class PushedVC: UIViewController {
override var canBecomeFirstResponder: Bool {
return true
}
override var canResignFirstResponder: Bool {
return true
}
override var inputAccessoryView: UIView? {
return UIView()
}
}
So to reiterate - If I push pushedVC onto the navController, pop it, present PresentedVC the emitters emit, but then the call to emitter.add(animation, forKey: nil)
is essentially ignored. The emitter just keeps emitting.
Here are some sample happy print statements from the completion block:
fade beginning -- delta: 1.016232967376709
fade beginning -- delta: 1.0033869743347168
fade beginning -- delta: 1.0054619312286377
fade beginning -- delta: 1.0080779790878296
fade beginning -- delta: 1.0088880062103271
fade beginning -- delta: 0.9923020601272583
fade beginning -- delta: 0.99943196773529
Here are my findings:
- The issue presents only when the pushed VC has an inputAccessoryView AND canBecomeFirstResponder is true
- It does not matter if the inputAccessoryView is UIKit or custom, has size, is visible, or anything.
- When I dismiss PresentedVC the animation is completed and the print statements show. Here are some unhappy print examples:
fade beginning -- delta: 5.003802061080933
fade beginning -- delta: 5.219511032104492
fade beginning -- delta: 5.73025906085968
fade beginning -- delta: 4.330522060394287
fade beginning -- delta: 4.786169052124023
- CATransaction.flush() does not fix anything
- Removing the entire CATransaction block and just calling
emitter.add(animation, forKey: nil)
similarly does nothing - the birth rate decrease animation does not happen
I am having trouble creating a simple demo project where the issue is reproducible (it is 100% reproducible in my code, the entirety of which I'm not going to link here) so I think getting a "solution" is unrealistic. What I would love is if anyone had any suggestions on where else to look? Any ways to debug CAAnimation? I think if I can solve the last bullet - emitter.add(animation, forKey: nil)
called w/o a CATransaction - I can break this whole thing. Why would a CAAnimation added directly to the layer which is visible and doing stuff refuse to run?