axiom-uikit-animation-debugging
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUIKit 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)
- in completion handler and you're not sure why
[weak self] - ❌ 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 offsetWhat 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:
- 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
在修改任何代码之前,你必须确定哪一项诊断结果是根本原因:
- 如果完成回调触发但实际时长 != 声明时长 → 应用模式2(CATransaction)
- 如果完成回调未触发且isRemovedOnCompletion为true → 应用模式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 AnimationCAAnimation 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 AnimationCommon 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+)
-
始终从模式1开始(完成回调基础)
- 如果完成回调从未触发 → 模式1
- 用打印语句验证完成回调是在add()之前设置的(第33行)
- 仅当完成回调触发但时序错误时,才继续应用模式2
-
然后是模式2(CATransaction时长不匹配)
- 仅当完成回调触发但实际时长 != 声明时长时使用
- 检查强制第一步中的日志(第40-47行)
-
然后是模式3(isRemovedOnCompletion)
- 仅当动画完成但视觉状态恢复原状时使用
-
模式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.5swift
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 wrappingWhy 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 stateswift
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 preservedWhy By default, animations are removed and layer reverts. For permanent state changes, set and .
isRemovedOnCompletion = falsefillMode = .forwardsswift
anim.isRemovedOnCompletion = false
anim.fillMode = .forwards // Keep final state after animation
layer.add(anim, forKey: nil)
// After 0.5s, animation state is preserved原因 默认情况下,动画会被移除,图层恢复原状。对于永久状态变化,设置和。
isRemovedOnCompletion = falsefillMode = .forwardsPattern 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 keyWhy 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 desyncswift
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 12swift
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
快速参考表
| Issue | Check | Fix |
|---|---|---|
| Completion never fires | Set handler BEFORE | Move |
| Duration mismatch | Is CATransaction wrapping? | Remove CATransaction or remove animation from it |
| Jank on older devices | Is value hardcoded? | Use |
| Animation disappears | | Set to |
| Gesture + animation jank | Synced updates? | Use |
| Multiple animations conflict | Same key? | Use unique keys or |
| Weak self in handler | Completion captured correctly? | Always use |
| 问题 | 检查项 | 修复方案 |
|---|---|---|
| 完成回调从未触发 | 回调是否在 | 将 |
| 时长不匹配 | 是否有CATransaction包装 | 移除CATransaction或将动画从其中移出 |
| 旧设备上卡顿 | 是否硬编码值 | 使用 |
| 动画消失 | | 设置为 |
| 手势+动画卡顿 | 是否同步更新? | 使用 |
| 多个动画冲突 | 是否使用相同键? | 使用唯一键或先调用 |
| 回调中的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
停止操作。你要么
- Skipped a mandatory step (most common)
- Misinterpreted diagnostic output
- Applied wrong pattern for your symptom
- Are in the 5% edge case requiring Instruments profiling
- 跳过了某个强制步骤(最常见)
- 误读了诊断输出
- 针对症状应用了错误的模式
- 遇到了需要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 or test class
ProcessInfo.processInfo.processorCount
❌ 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 with guard
[weak self]
❌ Not removing old animation before adding new
- Same keyPath replaces previous animation
- Fix: first or use unique keys
layer.removeAnimation(forKey:)
❌ 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
- 修复:始终使用并配合guard语句
[weak self]
❌ 添加新动画前未移除旧动画
- 相同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