axiom-uikit-animation-debugging

2
1
Source

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.zip

Installs 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:

  1. If completion fires but elapsed time != declared duration → Apply Pattern 2 (CATransaction)
  2. If completion doesn't fire AND isRemovedOnCompletion is true → Apply Pattern 3
  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

  1. 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
  2. Then Pattern 2 (CATransaction duration mismatch)

    • Only if completion fires but elapsed time != declared duration
    • Check logs from Mandatory First Steps (line 40-47)
  3. Then Pattern 3 (isRemovedOnCompletion)

    • Only if animation completes but visual state reverts
  4. 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.*

axiom-swiftui-nav-diag

CharlesWiltgen

Use when debugging navigation not responding, unexpected pops, deep links showing wrong screen, state lost on tab switch or background, crashes in navigationDestination, or any SwiftUI navigation failure - systematic diagnostics with production crisis defense

54

axiom-swiftui-26-ref

CharlesWiltgen

Use when implementing iOS 26 SwiftUI features - covers Liquid Glass design system, performance improvements, @Animatable macro, 3D spatial layout, scene bridging, WebView/WebPage, AttributedString rich text editing, drag and drop enhancements, and visionOS integration for iOS 26+

33

axiom-extensions-widgets-ref

CharlesWiltgen

Use when implementing widgets, Live Activities, Control Center controls, or app extensions - comprehensive API reference for WidgetKit, ActivityKit, App Groups, and extension lifecycle for iOS 14+

13

axiom-ios-build

CharlesWiltgen

Use when ANY iOS build fails, test crashes, Xcode misbehaves, or environment issue occurs before debugging code. Covers build failures, compilation errors, dependency conflicts, simulator problems, environment-first diagnostics.

253

axiom-camera-capture-ref

CharlesWiltgen

Reference — AVCaptureSession, AVCapturePhotoSettings, AVCapturePhotoOutput, RotationCoordinator, photoQualityPrioritization, deferred processing, AVCaptureMovieFileOutput, session presets, capture device APIs

42

axiom-swiftdata

CharlesWiltgen

Use when working with SwiftData - @Model definitions, @Query in SwiftUI, @Relationship macros, ModelContext patterns, CloudKit integration, iOS 26+ features, and Swift 6 concurrency with @MainActor — Apple's native persistence framework

12

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.

1,6851,430

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."

1,2711,335

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.

1,5441,153

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.

1,359809

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.

1,265728

pdf-to-markdown

aliceisjustplaying

Convert entire PDF documents to clean, structured Markdown for full context loading. Use this skill when the user wants to extract ALL text from a PDF into context (not grep/search), when discussing or analyzing PDF content in full, when the user mentions "load the whole PDF", "bring the PDF into context", "read the entire PDF", or when partial extraction/grepping would miss important context. This is the preferred method for PDF text extraction over page-by-page or grep approaches.

1,495685