axiom-uikit-animation-debugging
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
Install
mkdir -p .claude/skills/axiom-uikit-animation-debugging && curl -L -o skill.zip "https://mcp.directory/api/skills/download/3653" && unzip -o skill.zip -d .claude/skills/axiom-uikit-animation-debugging && rm skill.zipInstalls to .claude/skills/axiom-uikit-animation-debugging
About this skill
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):
// 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 let anim = layer.animation(forKey: key) {
print("\(key): duration=\(anim.duration), removed=\(anim.isRemovedOnCompletion)")
}
}
}
// 4. Check layer state
print("Layer speed: \(layer.speed)") // != 1.0 means timing is scaled
print("Layer timeOffset: \(layer.timeOffset)") // != 0 means animation is offset
What this tells you
- Completion print appears → Handler fires, issue is in callback code
- Completion print missing → Handler not firing, check CATransaction/layer state
- Elapsed time == declared → Duration is correct, visual jank is from frames
- Elapsed time != declared → CATransaction wrapping is changing duration
- layer.speed != 1.0 → Something is slowing animation
- Active animations list is long → Multiple animations competing
MANDATORY INTERPRETATION
Before changing ANY code, you must identify which ONE diagnostic is the root cause:
- If completion fires but elapsed time != declared duration → Apply Pattern 2 (CATransaction)
- If completion doesn't fire AND isRemovedOnCompletion is true → Apply Pattern 3
- If completion fires but visual is janky → MUST profile with Instruments first
- You cannot guess "it's probably frames" - prove it with data
- Profile > Core Animation instrument shows frame drops with certainty
- If you skip Instruments, you're guessing
If diagnostics are contradictory or unclear
- STOP. Do NOT proceed to patterns yet
- Add more print statements to narrow the cause
- Ask: "The diagnostics show X and Y but Z doesn't match. What am I missing?"
- Profile with Instruments > Core Animation if unsure
Decision Tree
CAAnimation problem?
├─ Completion handler never fires?
│ ├─ On simulator only?
│ │ └─ Simulator timing is different (60Hz). Test on real device.
│ ├─ On real device only?
│ │ ├─ Check: isRemovedOnCompletion and fillMode
│ │ ├─ Check: CATransaction wrapping
│ │ └─ Check: app goes to background during animation
│ └─ On both simulator and device?
│ ├─ Check: completion handler is set BEFORE adding animation
│ └─ Check: [weak self] is actually captured (not nil before completion)
│
├─ Duration mismatch (declared != visual)?
│ ├─ Is layer.speed != 1.0?
│ │ └─ Something scaled animation duration. Find and fix.
│ ├─ Is animation wrapped in CATransaction?
│ │ └─ CATransaction.setAnimationDuration() overrides animation.duration
│ └─ Is visual duration LONGER than declared?
│ └─ Simulator (60Hz) vs device frame rate (120Hz). Recalculate for real hardware.
│
├─ Spring physics wrong on device?
│ ├─ Are values hardcoded for one device?
│ │ └─ Use device performance class, not model
│ ├─ Are damping/stiffness values swapped with mass/stiffness?
│ │ └─ Check CASpringAnimation parameter meanings
│ └─ Does it work on simulator but not device?
│ └─ Simulator uses 60Hz. Device may use 120Hz. Recalculate.
│
└─ Gesture + animation jank?
├─ Are animations competing (same keyPath)?
│ └─ Remove old animation before adding new
├─ Is gesture updating layer while animation runs?
│ └─ Use CADisplayLink for synchronized updates
└─ Is gesture blocking the main thread?
└─ Profile with Instruments > Core Animation
Common Patterns
Pattern Selection Rules (MANDATORY)
Apply ONE pattern at a time, in this order
-
Always start with Pattern 1 (Completion Handler Basics)
- If completion NEVER fires → Pattern 1
- Verify completion is set BEFORE add() with print statement (line 33)
- Only proceed to Pattern 2 if completion FIRES but timing is wrong
-
Then Pattern 2 (CATransaction duration mismatch)
- Only if completion fires but elapsed time != declared duration
- Check logs from Mandatory First Steps (line 40-47)
-
Then Pattern 3 (isRemovedOnCompletion)
- Only if animation completes but visual state reverts
-
Patterns 4-7 Apply based on specific symptom (see Decision Tree line 91+)
FORBIDDEN
- ❌ Applying multiple patterns at once ("let me try Pattern 2 AND Pattern 4 together")
- ❌ Skipping Pattern 1 because "I already know it's not that"
- ❌ Combining patterns without understanding why each is needed
- ❌ Trying patterns randomly and hoping one works
Pattern 1: Completion Handler Basics
❌ WRONG (Handler set AFTER adding animation)
layer.add(animation, forKey: "myAnimation")
animation.completion = { finished in // ❌ Too late!
print("Done")
}
✅ CORRECT (Handler set BEFORE adding)
animation.completion = { [weak self] finished in
print("🔥 Animation finished: \(finished)")
guard let self = self else { return }
self.doNextStep()
}
layer.add(animation, forKey: "myAnimation")
Why Completion handler must be set before animation is added to layer. Setting after does nothing.
Pattern 2: CATransaction vs animation.duration
❌ WRONG (CATransaction overrides animation duration)
CATransaction.begin()
CATransaction.setAnimationDuration(2.0) // ❌ Overrides all animations!
let anim = CABasicAnimation(keyPath: "position")
anim.duration = 0.5 // This is ignored
layer.add(anim, forKey: nil)
CATransaction.commit() // Animation takes 2.0 seconds, not 0.5
✅ CORRECT (Set duration on animation, not transaction)
let anim = CABasicAnimation(keyPath: "position")
anim.duration = 0.5
layer.add(anim, forKey: nil)
// No CATransaction wrapping
Why CATransaction.setAnimationDuration() affects ALL animations in the transaction block. Use it only if you want to change all animations uniformly.
Pattern 3: isRemovedOnCompletion & fillMode
❌ WRONG (Animation disappears after completion)
let anim = CABasicAnimation(keyPath: "opacity")
anim.fromValue = 1.0
anim.toValue = 0.0
anim.duration = 0.5
layer.add(anim, forKey: nil)
// After 0.5s, animation is removed AND layer reverts to original state
✅ CORRECT (Keep animation state)
anim.isRemovedOnCompletion = false
anim.fillMode = .forwards // Keep final state after animation
layer.add(anim, forKey: nil)
// After 0.5s, animation state is preserved
Why By default, animations are removed and layer reverts. For permanent state changes, set isRemovedOnCompletion = false and fillMode = .forwards.
Pattern 4: Weak Self in Completion (MANDATORY)
❌ FORBIDDEN (Strong self creates retain cycle)
anim.completion = { finished in
self.property = "value" // ❌ GUARANTEED retain cycle
}
✅ MANDATORY (Always use weak self)
anim.completion = { [weak self] finished in
guard let self = self else { return }
self.property = "value" // Safe to access
}
Why this is MANDATORY, not optional
- CAAnimation keeps completion handler alive until animation completes
- Completion handler captures self strongly (unless explicitly weak)
- Creates retain cycle: self → animation → completion → self
- Memory leak occurs even if animation is short-lived (0.3s doesn't prevent it)
FORBIDDEN rationalizations
- ❌ "Animation is short, so no retain cycle risk"
- ❌ "I'll remove the animation manually, so it's fine"
- ❌ "This code path only runs once"
ALWAYS use [weak self] in completion handlers. No exceptions.
Pattern 5: Multiple Animations (Same keyPath)
❌ WRONG (Animations conflict)
// Add animation 1
let anim1 = CABasicAnimation(keyPath: "position.x")
anim1.toValue = 100
layer.add(anim1, forKey: "slide")
// Later, add animation 2
let anim2 = CABasicAnimation(keyPath: "position.x")
anim2.toValue = 200
layer.add(anim2, forKey: "slide") // ❌ Same key, replaces anim1!
✅ CORRECT (Remove before adding)
layer.removeAnimation(forKey: "slide") // Remove old first
let anim2 = CABasicAnimation(keyPath: "position.x")
anim2.toValue = 200
layer.add(anim2, forKey: "slide")
Or use unique keys:
let anim1 = CABasicAnimation(keyPath: "position.x")
layer.add(anim1, forKey: "
---
*Content truncated.*
More by CharlesWiltgen
View all skills by CharlesWiltgen →You might also like
flutter-development
aj-geddes
Build beautiful cross-platform mobile apps with Flutter and Dart. Covers widgets, state management with Provider/BLoC, navigation, API integration, and material design.
drawio-diagrams-enhanced
jgtolentino
Create professional draw.io (diagrams.net) diagrams in XML format (.drawio files) with integrated PMP/PMBOK methodologies, extensive visual asset libraries, and industry-standard professional templates. Use this skill when users ask to create flowcharts, swimlane diagrams, cross-functional flowcharts, org charts, network diagrams, UML diagrams, BPMN, project management diagrams (WBS, Gantt, PERT, RACI), risk matrices, stakeholder maps, or any other visual diagram in draw.io format. This skill includes access to custom shape libraries for icons, clipart, and professional symbols.
ui-ux-pro-max
nextlevelbuilder
"UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient."
godot
bfollington
This skill should be used when working on Godot Engine projects. It provides specialized knowledge of Godot's file formats (.gd, .tscn, .tres), architecture patterns (component-based, signal-driven, resource-based), common pitfalls, validation tools, code templates, and CLI workflows. The `godot` command is available for running the game, validating scripts, importing resources, and exporting builds. Use this skill for tasks involving Godot game development, debugging scene/resource files, implementing game systems, or creating new Godot components.
nano-banana-pro
garg-aayush
Generate and edit images using Google's Nano Banana Pro (Gemini 3 Pro Image) API. Use when the user asks to generate, create, edit, modify, change, alter, or update images. Also use when user references an existing image file and asks to modify it in any way (e.g., "modify this image", "change the background", "replace X with Y"). Supports both text-to-image generation and image-to-image editing with configurable resolution (1K default, 2K, or 4K for high resolution). DO NOT read the image file first - use this skill directly with the --input-image parameter.
fastapi-templates
wshobson
Create production-ready FastAPI projects with async patterns, dependency injection, and comprehensive error handling. Use when building new FastAPI applications or setting up backend API projects.
Related MCP Servers
Browse all serversDeepSeek offers an AI-powered chatbot and writing assistant for chat completions, writing help, and code generation with
TaskManager streamlines project tracking and time management with efficient task queues, ideal for managing projects sof
Integrate Claude with any OpenAI-compatible chat API and SDKs (OpenAI, Perplexity, Groq, xAI, PyroPrompts). Easy Claude
Access chat completions with accurate citations using the Perplexity API. Enhance your applications with reliable AI-pow
Rtfmbro is an MCP server for config management tools—get real-time, version-specific docs from GitHub for Python, Node.j
Freepik integration for searching and downloading icons, managing design assets, and generating images via text-to-image
Stay ahead of the MCP ecosystem
Get weekly updates on new skills and servers.