Use when CAAnimation completion handler doesn't fire, spring physics look wrong on device, animation duration mismatches actual time, gesture + animation interaction causes jank, or timing differs between simulator and real hardware - systematic CAAnimation diagnosis with CATransaction patterns, frame rate awareness, and device-specific behavior
View on GitHubCharlesWiltgen/Axiom
axiom
.claude-plugin/plugins/axiom/skills/axiom-uikit-animation-debugging/SKILL.md
January 16, 2026
Select agents to install to:
npx add-skill https://github.com/CharlesWiltgen/Axiom/blob/main/.claude-plugin/plugins/axiom/skills/axiom-uikit-animation-debugging/SKILL.md -a claude-code --skill axiom-uikit-animation-debuggingInstallation paths:
.claude/skills/axiom-uikit-animation-debugging/# UIKit Animation Debugging
## Overview
CAAnimation issues manifest as missing completion handlers, wrong timing, or jank under specific conditions. **Core principle** 90% of CAAnimation problems are CATransaction timing, layer state, or frame rate assumptions, not Core Animation bugs.
## Red Flags โ Suspect CAAnimation Issue
If you see ANY of these, suspect animation logic not device behavior:
- Completion handler fires on simulator but not device
- Animation duration (0.5s) doesn't match visual duration (1.2s)
- Spring animation looks correct on iPhone 15 Pro but janky on older devices
- Gesture + animation together causes stuttering (fine separately)
- `[weak self]` in completion handler and you're not sure why
- โ **FORBIDDEN** Hardcoding duration/values to "match what actually happens"
- This ships device-specific bugs to users on different hardware
- Do not rationalize this as a "temporary fix" or "good enough"
**Critical distinction** Simulator often hides timing issues (60Hz only, no throttling). Real devices expose them (variable frame rate, CPU throttling, background pressure). **MANDATORY: Test on real device (oldest supported model) before shipping.**
## Mandatory First Steps
**ALWAYS run these FIRST** (before changing code):
```swift
// 1. Check if completion is firing at all
animation.completion = { [weak self] finished in
print("๐ฅ COMPLETION FIRED: finished=\(finished)")
guard let self = self else {
print("๐ฅ SELF WAS NIL")
return
}
// original code
}
// 2. Check actual duration vs declared
let startTime = Date()
let anim = CABasicAnimation(keyPath: "position.x")
anim.duration = 0.5 // Declared
layer.add(anim, forKey: "test")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.51) {
print("Elapsed: \(Date().timeIntervalSince(startTime))") // Actual
}
// 3. Check what animations are active
if let keys = layer.animationKeys() {
print("Active animations: \(keys)")
for key in keys {
if l