axiom-uikit-animation-debugging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

UIKit Animation Debugging

UIKit 动画调试

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.
CAAnimation问题表现为特定条件下缺失完成回调、时序错误或卡顿。核心原则90%的CAAnimation问题源于CATransaction时序、图层状态或帧率假设,而非Core Animation本身的bug。

Red Flags — Suspect CAAnimation Issue

预警信号——怀疑CAAnimation问题

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.
如果出现以下任何一种情况,要怀疑是动画逻辑问题而非设备行为:
  • 完成回调在模拟器上触发但在设备上不触发
  • 动画时长(0.5秒)与视觉时长(1.2秒)不匹配
  • 弹簧动画在iPhone 15 Pro上显示正常但在旧设备上卡顿
  • 手势+动画一起使用时出现卡顿(单独使用时正常)
  • 完成回调中使用了
    [weak self]
    但你不确定原因
  • 禁止 硬编码时长/值以“匹配实际发生的情况”
    • 这会将设备特定的bug交付给使用不同硬件的用户
    • 不要将其合理化作为“临时修复”或“足够好”的方案
关键区别模拟器通常会隐藏时序问题(仅60Hz,无降频)。真实设备会暴露这些问题(可变帧率、CPU降频、后台压力)。强制要求:上线前必须在真实设备(支持的最旧机型)上测试。

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 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
始终先执行这些步骤(在修改代码之前):
swift
// 1. 检查完成回调是否触发
animation.completion = { [weak self] finished in
    print("🔥 COMPLETION FIRED: finished=\(finished)")
    guard let self = self else {
        print("🔥 SELF WAS NIL")
        return
    }
    // original code
}

// 2. 检查实际时长与声明时长是否一致
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. 检查活跃的动画
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. 检查图层状态
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
  • 出现完成回调打印 → 回调已触发,问题出在回调代码中
  • 无完成回调打印 → 回调未触发,检查CATransaction/图层状态
  • 实际时长 == 声明时长 → 时长正确,视觉卡顿源于帧率问题
  • 实际时长 != 声明时长 → CATransaction包装修改了时长
  • layer.speed != 1.0 → 有因素在减慢动画
  • 活跃动画列表很长 → 多个动画在竞争资源

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
在修改任何代码之前,你必须确定哪一项诊断结果是根本原因:
  1. 如果完成回调触发但实际时长 != 声明时长 → 应用模式2(CATransaction)
  2. 如果完成回调未触发且isRemovedOnCompletion为true → 应用模式3
  3. 如果完成回调触发但视觉效果卡顿 → 必须先使用Instruments进行性能分析
    • 你不能猜测“可能是帧率问题”——要用数据证明
    • 性能分析 > Core Animation工具可以明确显示掉帧情况
    • 跳过Instruments就是在猜测

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
  • 停止操作。暂时不要应用任何模式
  • 添加更多打印语句来缩小原因范围
  • 自问:“诊断结果显示X和Y,但Z不匹配。我漏掉了什么?”
  • 若不确定,使用Instruments > Core Animation进行性能分析

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
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+)
  1. 始终从模式1开始(完成回调基础)
    • 如果完成回调从未触发 → 模式1
    • 用打印语句验证完成回调是在add()之前设置的(第33行)
    • 仅当完成回调触发但时序错误时,才继续应用模式2
  2. 然后是模式2(CATransaction时长不匹配)
    • 仅当完成回调触发但实际时长 != 声明时长时使用
    • 检查强制第一步中的日志(第40-47行)
  3. 然后是模式3(isRemovedOnCompletion)
    • 仅当动画完成但视觉状态恢复原状时使用
  4. 模式4-7 根据特定症状应用(见决策树第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
  • ❌ 同时应用多种模式(“我试试同时用模式2和模式4”)
  • ❌ 跳过模式1,因为“我已经知道不是这个问题”
  • ❌ 不理解每种模式的用途就组合使用
  • ❌ 随机尝试模式,寄希望于其中一种有效

Pattern 1: Completion Handler Basics

模式1:完成回调基础

❌ WRONG (Handler set AFTER adding animation)

❌ 错误(回调在添加动画后设置)

swift
layer.add(animation, forKey: "myAnimation")
animation.completion = { finished in  // ❌ Too late!
    print("Done")
}
swift
layer.add(animation, forKey: "myAnimation")
animation.completion = { finished in  // ❌ Too late!
    print("Done")
}

✅ CORRECT (Handler set BEFORE adding)

✅ 正确(回调在添加动画前设置)

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

swift
animation.completion = { [weak self] finished in
    print("🔥 Animation finished: \(finished)")
    guard let self = self else { return }
    self.doNextStep()
}
layer.add(animation, forKey: "myAnimation")
原因 完成回调必须在动画添加到图层之前设置。之后设置不会起作用。

Pattern 2: CATransaction vs animation.duration

模式2:CATransaction vs animation.duration

❌ WRONG (CATransaction overrides animation duration)

❌ 错误(CATransaction覆盖动画时长)

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

✅ 正确(在动画上设置时长,而非事务)

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

swift
let anim = CABasicAnimation(keyPath: "position")
anim.duration = 0.5
layer.add(anim, forKey: nil)
// No CATransaction wrapping
原因 CATransaction.setAnimationDuration()会影响事务块中的所有动画。仅当你想统一修改所有动画时才使用它。

Pattern 3: isRemovedOnCompletion & fillMode

模式3:isRemovedOnCompletion & fillMode

❌ WRONG (Animation disappears after completion)

❌ 错误(动画完成后消失)

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

✅ 正确(保留动画状态)

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

swift
anim.isRemovedOnCompletion = false
anim.fillMode = .forwards  // Keep final state after animation
layer.add(anim, forKey: nil)
// After 0.5s, animation state is preserved
原因 默认情况下,动画会被移除,图层恢复原状。对于永久状态变化,设置
isRemovedOnCompletion = false
fillMode = .forwards

Pattern 4: Weak Self in Completion (MANDATORY)

模式4:完成回调中的Weak Self(强制)

❌ FORBIDDEN (Strong self creates retain cycle)

❌ 禁止(强引用self会导致循环引用)

swift
anim.completion = { finished in
    self.property = "value"  // ❌ GUARANTEED retain cycle
}
swift
anim.completion = { finished in
    self.property = "value"  // ❌ GUARANTEED retain cycle
}

✅ MANDATORY (Always use weak self)

✅ 强制(始终使用weak self)

swift
anim.completion = { [weak self] finished in
    guard let self = self else { return }
    self.property = "value"  // Safe to access
}
swift
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)
  • CAAnimation会保持完成回调存活直到动画完成
  • 完成回调会强引用self(除非显式声明为weak)
  • 形成循环引用:self → animation → completion → self
  • 即使动画很短(0.3秒)也会导致内存泄漏

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.

始终在完成回调中使用[weak self]。没有例外。



Pattern 5: Multiple Animations (Same keyPath)

模式5:多个动画(同一keyPath)

❌ WRONG (Animations conflict)

❌ 错误(动画冲突)

swift
// 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!
swift
// 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)

✅ 正确(添加前先移除旧动画)

swift
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:
swift
let anim1 = CABasicAnimation(keyPath: "position.x")
layer.add(anim1, forKey: "slide_1")

let anim2 = CABasicAnimation(keyPath: "position.x")
layer.add(anim2, forKey: "slide_2")  // Different key
Why Adding animation with same key replaces previous animation. Either remove old animation or use unique keys.

swift
layer.removeAnimation(forKey: "slide")  // Remove old first

let anim2 = CABasicAnimation(keyPath: "position.x")
anim2.toValue = 200
layer.add(anim2, forKey: "slide")
或者使用唯一键:
swift
let anim1 = CABasicAnimation(keyPath: "position.x")
layer.add(anim1, forKey: "slide_1")

let anim2 = CABasicAnimation(keyPath: "position.x")
layer.add(anim2, forKey: "slide_2")  // Different key
原因 使用相同键添加动画会替换之前的动画。要么移除旧动画,要么使用唯一键。

Pattern 6: CADisplayLink for Gesture + Animation Sync

模式6:使用CADisplayLink同步手势与动画

❌ WRONG (Gesture updates directly, animation updates at different rate)

❌ 错误(手势直接更新,动画以不同速率更新)

swift
func handlePan(_ gesture: UIPanGestureRecognizer) {
    let translation = gesture.translation(in: view)
    view.layer.position.x = translation.x  // ❌ Syncing issue
}

// Separately:
let anim = CABasicAnimation(keyPath: "position.x")
view.layer.add(anim, forKey: nil)  // Jank from desync
swift
func handlePan(_ gesture: UIPanGestureRecognizer) {
    let translation = gesture.translation(in: view)
    view.layer.position.x = translation.x  // ❌ Syncing issue
}

// Separately:
let anim = CABasicAnimation(keyPath: "position.x")
view.layer.add(anim, forKey: nil)  // Jank from desync

✅ CORRECT (Use CADisplayLink for synchronization)

✅ 正确(使用CADisplayLink进行同步)

swift
var displayLink: CADisplayLink?

func startSyncedAnimation() {
    displayLink = CADisplayLink(
        target: self,
        selector: #selector(updateAnimation)
    )
    displayLink?.add(to: .main, forMode: .common)
}

@objc func updateAnimation() {
    // Update gesture AND animation in same frame
    let gesture = currentGesture
    let position = calculatePosition(from: gesture)
    layer.position = position  // Synchronized update
}
Why Gesture recognizer and CAAnimation may run at different frame rates. CADisplayLink syncs both to screen refresh rate.

swift
var displayLink: CADisplayLink?

func startSyncedAnimation() {
    displayLink = CADisplayLink(
        target: self,
        selector: #selector(updateAnimation)
    )
    displayLink?.add(to: .main, forMode: .common)
}

@objc func updateAnimation() {
    // Update gesture AND animation in same frame
    let gesture = currentGesture
    let position = calculatePosition(from: gesture)
    layer.position = position  // Synchronized update
}
原因 手势识别器和CAAnimation可能以不同帧率运行。CADisplayLink将两者同步到屏幕刷新率。

Pattern 7: Spring Animation Device Differences

模式7:弹簧动画的设备差异

❌ WRONG (Hardcoded for one device)

❌ 错误(为单一设备硬编码值)

swift
let springAnim = CASpringAnimation()
springAnim.damping = 0.7  // Hardcoded for iPhone 15 Pro
springAnim.stiffness = 100
layer.add(springAnim, forKey: nil)  // Janky on iPhone 12
swift
let springAnim = CASpringAnimation()
springAnim.damping = 0.7  // Hardcoded for iPhone 15 Pro
springAnim.stiffness = 100
layer.add(springAnim, forKey: nil)  // Janky on iPhone 12

✅ CORRECT (Adapt to device performance)

✅ 正确(适配设备性能)

swift
let springAnim = CASpringAnimation()

// Use device performance class, not model
if ProcessInfo.processInfo.processorCount >= 6 {
    // Modern A-series (A14+)
    springAnim.damping = 0.7
    springAnim.stiffness = 100
} else {
    // Older A-series
    springAnim.damping = 0.85
    springAnim.stiffness = 80
}

layer.add(springAnim, forKey: nil)
Why Spring physics feel different at 60Hz vs 120Hz. Use device class (core count, GPU) not model.

swift
let springAnim = CASpringAnimation()

// Use device performance class, not model
if ProcessInfo.processInfo.processorCount >= 6 {
    // Modern A-series (A14+)
    springAnim.damping = 0.7
    springAnim.stiffness = 100
} else {
    // Older A-series
    springAnim.damping = 0.85
    springAnim.stiffness = 80
}

layer.add(springAnim, forKey: nil)
原因 弹簧物理效果在60Hz和120Hz下的表现不同。使用设备类别(核心数、GPU)而非型号。

Quick Reference Table

快速参考表

IssueCheckFix
Completion never firesSet handler BEFORE
add()
Move
completion =
before
add()
Duration mismatchIs CATransaction wrapping?Remove CATransaction or remove animation from it
Jank on older devicesIs value hardcoded?Use
ProcessInfo
for device class
Animation disappears
isRemovedOnCompletion
?
Set to
false
, use
fillMode = .forwards
Gesture + animation jankSynced updates?Use
CADisplayLink
Multiple animations conflictSame key?Use unique keys or
removeAnimation()
first
Weak self in handlerCompletion captured correctly?Always use
[weak self]
in completion
问题检查项修复方案
完成回调从未触发回调是否在
add()
之前设置
completion =
移到
add()
之前
时长不匹配是否有CATransaction包装移除CATransaction或将动画从其中移出
旧设备上卡顿是否硬编码值使用
ProcessInfo
获取设备类别
动画消失
isRemovedOnCompletion
设置?
设置为
false
,使用
fillMode = .forwards
手势+动画卡顿是否同步更新?使用
CADisplayLink
多个动画冲突是否使用相同键?使用唯一键或先调用
removeAnimation()
回调中的Weak Self完成回调是否正确捕获?始终在完成回调中使用
[weak self]

When You're Stuck After 30 Minutes

当你卡壳超过30分钟时

If you've spent >30 minutes and the animation is still broken:
如果你已经花费了30多分钟,动画仍然有问题:

STOP. You either

停止操作。你要么

  1. Skipped a mandatory step (most common)
  2. Misinterpreted diagnostic output
  3. Applied wrong pattern for your symptom
  4. Are in the 5% edge case requiring Instruments profiling
  1. 跳过了某个强制步骤(最常见)
  2. 误读了诊断输出
  3. 针对症状应用了错误的模式
  4. 遇到了需要Instruments性能分析的5%边缘情况

MANDATORY checklist before claiming "skill didn't work"

在声称“这个方法没用”之前,必须检查以下清单

  • I ran ALL 4 diagnostic blocks from Mandatory First Steps (lines 28-63)
  • I pasted the EXACT output of diagnostics (logs, print statements)
  • I identified ONE root cause from "What this tells you" (lines 66-72)
  • I applied the FIRST matching pattern from Decision Tree (lines 91+)
  • I tested the pattern on a REAL device, not just simulator
  • I verified the pattern with print statements/logs showing the fix worked
  • 我运行了强制第一步中的所有4个诊断代码块(第28-63行)
  • 我粘贴了诊断的精确输出(日志、打印语句)
  • 我从“这些诊断能告诉你什么”中确定了一个根本原因(第66-72行)
  • 我从决策树中应用了第一个匹配的模式(第91行及以后)
  • 我在真实设备上测试了模式,而非仅在模拟器上
  • 我通过打印语句/日志验证了模式修复有效

If ALL boxes are checked and still broken

如果所有框都已勾选但仍然有问题

  • You MUST profile with Instruments > Core Animation
  • Time cost: 30-60 minutes (unavoidable for edge cases)
  • Hardcoding, asyncAfter, or "shipping and hoping" are FORBIDDEN
  • Ask for guidance before adding any workarounds
  • 你必须使用Instruments > Core Animation进行性能分析
  • 时间成本:30-60分钟(边缘情况不可避免)
  • 硬编码、asyncAfter或“上线碰运气”都是禁止的
  • 在添加任何变通方案之前寻求指导

Time cost transparency

时间成本透明度

  • Pattern 1: 2-5 minutes
  • Pattern 2: 3-5 minutes
  • Instruments profiling: 30-60 minutes (for edge cases only)
  • Trying random fixes without profiling: 2-4 hours + risk of shipping broken
  • 模式1:2-5分钟
  • 模式2:3-5分钟
  • Instruments性能分析:30-60分钟(仅针对边缘情况)
  • 不进行分析就随机尝试修复:2-4小时 + 上线带bug的风险

Common Mistakes

常见错误

Setting completion handler AFTER adding animation
  • Completion is not set in time
  • Fix: Set completion BEFORE
    layer.add()
Assuming simulator timing = device timing
  • Simulator runs 60Hz, devices run 60Hz-120Hz
  • Fix: Test on real device before tuning duration
Hardcoding device-specific values
  • "This value works on iPhone 15 Pro" → fails on iPhone 12
  • Fix: Use
    ProcessInfo.processInfo.processorCount
    or test class
Wrapping animation in CATransaction.setAnimationDuration()
  • Overrides all animation durations in that transaction
  • Fix: Set duration on animation, not transaction
FORBIDDEN: Using strong self in completion handler
  • GUARANTEED retain cycle: self → animation → completion → self
  • Fix: ALWAYS use
    [weak self]
    with guard
Not removing old animation before adding new
  • Same keyPath replaces previous animation
  • Fix:
    layer.removeAnimation(forKey:)
    first or use unique keys
Ignoring layer.speed and layer.timeOffset
  • These scale animation timing invisibly
  • Fix: Check these values if timing is wrong
在添加动画后设置完成回调
  • 回调设置太晚,无法生效
  • 修复:在
    layer.add()
    之前设置
    completion
假设模拟器时序 = 设备时序
  • 模拟器运行60Hz,设备运行60Hz-120Hz
  • 修复:在调整时长前在真实设备上测试
硬编码设备特定值
  • “这个值在iPhone 15 Pro上有效” → 在iPhone 12上失效
  • 修复:使用
    ProcessInfo.processInfo.processorCount
    或测试类别
用CATransaction.setAnimationDuration()包装动画
  • 覆盖了该事务中所有动画的时长
  • 修复:在动画上设置时长,而非事务
禁止:在完成回调中使用强引用self
  • 必然导致循环引用:self → animation → completion → self
  • 修复:始终使用
    [weak self]
    并配合guard语句
添加新动画前未移除旧动画
  • 相同keyPath会替换之前的动画
  • 修复:先调用
    layer.removeAnimation(forKey:)
    或使用唯一键
忽略layer.speed和layer.timeOffset
  • 这些会无形地缩放动画时序
  • 修复:如果时序错误,检查这些值

Real-World Impact

实际影响

Before CAAnimation debugging 2-4 hours per issue
  • Print everywhere, test on simulator, hardcode values, ship and hope
  • "Maybe it's a device bug?"
  • DispatchQueue.asyncAfter as fallback timer
After 15-30 minutes with systematic diagnosis
  • Check completion handler setup (2 min)
  • Check CATransaction wrapping (3 min)
  • Check layer state and duration mismatch (5 min)
  • Identify root cause, apply pattern (5 min)
  • Test on real device (varies)
Key insight CAAnimation issues are almost always CATransaction, layer state, or frame rate assumptions, never Core Animation bugs.

Last Updated: 2025-11-30 Status: TDD-tested with pressure scenarios Framework: UIKit CAAnimation
之前 每个CAAnimation问题需要2-4小时调试
  • 到处打印,在模拟器上测试,硬编码值,上线碰运气
  • “也许是设备bug?”
  • 用DispatchQueue.asyncAfter作为备用计时器
之后 系统化诊断只需15-30分钟
  • 检查完成回调设置(2分钟)
  • 检查CATransaction包装(3分钟)
  • 检查图层状态和时长不匹配(5分钟)
  • 确定根本原因,应用模式(5分钟)
  • 在真实设备上测试(时间不定)
关键见解 CAAnimation问题几乎总是源于CATransaction、图层状态或帧率假设,而非Core Animation本身的bug。

Last Updated: 2025-11-30 Status: TDD-tested with pressure scenarios Framework: UIKit CAAnimation