mcpField
A
axiom-swiftui-debugging-diag

axiom-swiftui-debugging-diag

github.com/charleswiltgen开发1.2k182 次安装今天更新
代码仓库

关于 axiom-swiftui-debugging-diag

面向开发者的 SwiftUI 诊断技能,用于调查复杂的视图更新、身份、性能和预览故障。它结合临时日志与 Instruments 跟踪,并避免掩盖根因的修补方式。

主要功能

  1. 01Self._printChanges() 分析
  2. 02SwiftUI Instrument 跟踪
  3. 03视图身份调查
  4. 04环境依赖检查
  5. 05预览诊断

适用场景

  1. 01视图意外更新或更新过于频繁
  2. 02状态重置或动画失效
  3. 03追踪多个视图间的数据变化
  4. 04预览无明确错误却崩溃

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-debugging-diag

安装后在 Claude Code 中输入/axiom-swiftui-debugging-diag确认已加载。

详细介绍

SwiftUI 调试诊断

何时使用此诊断技能

在以下情况下使用此技能:

  • 基本故障排查未能解决问题 — 已应用 axiom-swiftui-debugging 技能模式,但问题仍然存在
  • Self._printChanges() 显示异常模式 — 视图在不该更新时更新,或该更新时却没有更新
  • 间歇性问题 — 有时正常,有时失败(“海森堡 Bug”)
  • 复杂依赖链 — 需要追踪数据在多个视图和模型间的流动
  • 性能调查 — 视图更新过于频繁或耗时过长
  • 预览异常 — 出现不明显的崩溃或故障

禁止的操作

遇到压力时,你可能会想走捷径,而不是诊断问题。绝对不要这样做:

❌ 随意猜测并更改 @State/@Observable

  • “我试试在这里添加 @Observable,看能不能解决”
  • “也许把它改成 @StateObject 就能修好”

❌ 添加 .id(UUID()) 强制更新

  • 每次渲染都会创建新的视图标识
  • 破坏状态保留
  • 掩盖根本原因

❌ 在 @Observable 可行时使用 ObservableObject(iOS 17+)

  • 增加不必要的复杂性
  • 无法使用自动依赖追踪

❌ 忽略间歇性问题(“有时能用”)

  • “我就直接合并吧,希望它在生产环境里别出问题”
  • 间歇性问题 = 系统性 Bug,而非随机现象

❌ 未理解问题就发布

  • “修复有效,但我不知道原因”
  • 生产环境经不起反复试错

必须先完成的步骤

开始诊断模式前,先确认基线环境:

# 1. Verify Instruments setup
xcodebuild -version  # Must be Xcode 26+ for SwiftUI Instrument

# 2. Build in Release mode for profiling
xcodebuild build -scheme YourScheme -configuration Release

# 3. Clear derived data if investigating preview issues
rm -rf ~/Library/Developer/Xcode/DerivedData

耗时:5 分钟 原因:错误的 Xcode 版本或 Debug 模式会产生误导性的性能分析数据


诊断决策树

SwiftUI view issue after basic troubleshooting?
│
├─ View not updating?
│  ├─ Basic check: Add Self._printChanges() temporarily
│  │  ├─ Shows "@self changed" → View value changed
│  │  │  └─ Pattern D1: Analyze what caused view recreation
│  │  ├─ Shows specific state property → That state triggered update
│  │  │  └─ Verify: Should that state trigger update?
│  │  └─ Nothing logged → Body not being called at all
│  │     └─ Pattern D3: View Identity Investigation
│  └─ Advanced: Use SwiftUI Instrument
│     └─ Pattern D2: SwiftUI Instrument Investigation
│
├─ View updating too often?
│  ├─ Pattern D1: Self._printChanges() Analysis
│  │  └─ Identify unnecessary state dependencies
│  └─ Pattern D2: SwiftUI Instrument → Cause & Effect Graph
│     └─ Trace data flow, find broad dependencies
│
├─ Intermittent issues (works sometimes)?
│  ├─ Pattern D3: View Identity Investigation
│  │  └─ Check: Does identity change unexpectedly?
│  ├─ Pattern D4: Environment Dependency Check
│  │  └─ Check: Environment values changing frequently?
│  └─ Reproduce in preview 30+ times
│     └─ If can't reproduce: Likely timing/race condition
│
└─ Preview crashes (after basic fixes)?
    ├─ Pattern D5: Preview Diagnostics (Xcode 26)
    │  └─ Check diagnostics button, crash logs
    └─ If still fails: Pattern D2 (profile preview build)

诊断模式

模式 D1:分析 Self._printChanges()

耗时:5 分钟

症状:需要准确了解视图主体为何运行

适用场景:

  • 视图更新频率超出预期
  • 视图在该更新时却没有更新
  • 重构后验证依赖关系

方法:

struct MyView: View {
    @State private var count = 0
    @Environment(AppModel.self) private var model

    var body: some View {
        let _ = Self._printChanges()  // Add temporarily

        VStack {
            Text("Count: \(count)")
            Text("Model value: \(model.value)")
        }
    }
}

输出解读:

# Scenario 1: View parameter changed
MyView: @self changed
→ Parent passed new MyView instance
→ Check parent code - what triggered recreation?

# Scenario 2: State property changed
MyView: count changed
→ Local @State triggered update
→ Expected if you modified count

# Scenario 3: Environment property changed
MyView: @self changed  # Environment is part of @self
→ Environment value changed (color scheme, locale, custom value)
→ Pattern D4: Check environment dependencies

# Scenario 4: Nothing logged
→ Body not being called
→ Pattern D3: View identity investigation

常见发现:

  1. 出现意料之外的“@self changed”

    • 父视图不必要地重新创建视图
    • 检查父视图的状态管理
  2. 属性显示已更改,但你并未更改它

    • 间接依赖(读取了发生变化的对象)
    • 模式 D2:使用 Instruments 追踪
  3. 多个属性同时发生变化

    • 依赖范围过宽(例如,需要的只是一个元素,却读取了整个数组)
    • 修复方法:提取特定依赖项

验证:

  • 提交前移除 Self._printChanges() 调用
  • 切勿将这段代码发布到生产环境

交叉参考:对于复杂情况,请使用模式 D2(SwiftUI Instrument)


模式 D2:SwiftUI Instrument 调查

耗时:25 分钟

症状:更新模式复杂,Self._printChanges() 无法完全解释

适用场景:

  • 一个视图更新时,多个视图也随之更新
  • 需要追踪应用中的数据流
  • 视图正在更新,但不知道是哪些数据触发了更新
  • 视图主体更新耗时较长(性能问题)

前提条件:

  • 已安装 Xcode 26+
  • 设备已更新至 iOS 26+ / macOS Tahoe+
  • 以 Release 模式构建

步骤:

1. 启动 Instruments(5 分钟)
# Build Release
xcodebuild build -scheme YourScheme -configuration Release

# Launch Instruments
# Press Command-I in Xcode
# Choose "SwiftUI" template
2. 录制跟踪(3 分钟)
  • 点击 Record 按钮
  • 执行触发意外更新的操作
  • 停止录制(交互 10-30 秒即可)
3. 分析耗时较长的视图主体更新(5 分钟)
  • 查看 Long View Body Updates 通道
  • 是否有橙色/红色条?这表示开销较大的视图
  • 点击一条耗时较长的更新 → 详细信息面板会显示视图名称
  • 右键点击 → “Set Inspection Range and Zoom”
  • 切换到 Time Profiler 跟踪
  • 在调用栈中找到你的视图
  • 找出开销较大的操作(创建格式化器、计算等)

修复方法:将开销较大的操作移至模型层,并缓存结果

4. 分析不必要的更新(7 分钟)
  • 突出显示用户操作的时间范围(例如,轻点收藏按钮)
  • 在详细信息窗格中展开层级结构
  • 统计更新次数——是否超出预期?
  • 将鼠标悬停在视图上 → 点击箭头 → “显示因果关系图”
5. 解读因果关系图(5 分钟)

图节点:

[Blue node] = Your code (gesture, state change, view body)
[System node] = SwiftUI/system work
[Arrow labeled "update"] = Caused this update
[Arrow labeled "creation"] = Caused view to appear

常见模式:

# Pattern A: Single view updates (GOOD)
[Gesture] → [State Change in ViewModelA] → [ViewA body]

# Pattern B: All views update (BAD - broad dependency)
[Gesture] → [Array change] → [All list item views update]
└─ Fix: Use granular view models, one per item

# Pattern C: Cascade through environment (CHECK)
[State Change] → [Environment write] → [Many view bodies check]
└─ If environment value changes frequently → Pattern D4 fix

点击节点:

  • 状态更改节点 → 查看该值的设置位置回溯
  • 视图主体节点 → 查看它读取了哪些属性(依赖项)

验证:

  • 修复后记录新的跟踪
  • 比较更新次数的修复前后变化
  • 确认红色/橙色条减少或消失

交叉参考:axiom-swiftui-performance 技能,了解详细的 Instruments 工作流


模式 D3:视图身份调查

耗时:15 分钟

症状:@State 值意外重置,或视图不执行动画

适用情况:

  • 计数器意外重置为 0
  • 动画无效(视图突然出现,而非以动画形式出现)
  • ForEach 项目跳动
  • 文本字段失去焦点

根本原因:视图身份意外发生变化

调查步骤:

1. 检查条件放置(5 分钟)
// ❌ PROBLEM: Identity changes with condition
if showDetails {
    CounterView()  // Gets new identity each time showDetails toggles
}

// ✅ FIX: Use .opacity()
CounterView()
    .opacity(showDetails ? 1 : 0)  // Same identity always

查找:在代码库中搜索包含状态的 if/else 内部视图

2. 检查 .id() 修饰符(5 分钟)
// ❌ PROBLEM: .id() changes when data changes
DetailView()
    .id(item.id + "-\(isEditing)")  // ID changes with isEditing

// ✅ FIX: Stable ID
DetailView()
    .id(item.id)  // Stable ID

查找:在代码库中搜索 .id(,检查 ID 值是否会变化

3. 检查 ForEach 标识符(5 分钟)
// ❌ WRONG: Index-based ID
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
    Text(item.name)
}

// ❌ WRONG: Non-unique ID
ForEach(items, id: \.category) { item in  // Multiple items per category
    Text(item.name)
}

// ✅ RIGHT: Unique, stable ID
ForEach(items, id: \.id) { item in
    Text(item.name)
}

查找:搜索 ForEach,验证 ID 是否唯一且稳定

修复模式:

问题修复方法
条件中的视图改用 .opacity()
.id() 变化过于频繁使用稳定的标识符
ForEach 项目跳动使用唯一且稳定的 ID(UUID 或服务器 ID)
导航时状态重置检查 NavigationStack 路径管理

验证:

  • 添加 Self._printChanges()——不应反复看到“@self changed”
  • 动画现在应能流畅运行
  • @State 值应能保持

模式 D4:环境依赖项检查

耗时:10 分钟

症状:无关数据变化时,许多视图都在更新

适用情况:

  • 因果关系图显示“Environment”节点触发了许多更新
  • 滚动或动画性能缓慢
  • 出现意外的级联更新

根本原因:环境中存在频繁变化的值,或者读取环境的视图过多

调查步骤:

1. 查找环境写入操作(3 分钟)
# Search for environment modifiers in current project
grep -r "\.environment(" --include="*.swift" .

查找:

// ❌ BAD: Frequently changing values
.environment(\.scrollOffset, scrollOffset)  // Updates 60+ times/second
.environment(model)  // If model updates frequently

// ✅ GOOD: Stable values
.environment(\.colorScheme, .dark)
.environment(appModel)  // If appModel changes rarely
2. 检查环境中的内容(3 分钟)

使用模式 D2(Instruments),检查因果关系图:

  • 点击“Environment”节点
  • 查看哪些属性发生了变化
  • 统计有多少视图检查了更新

问题:

  • 此值是否在每次滚动/动画帧中都会变化?
  • 这些视图是否都确实需要此值?
3. 应用修复(4 分钟)

修复 A:从环境中移除(如果变化频繁):

// ❌ Before: Environment
.environment(\.scrollOffset, scrollOffset)

// ✅ After: Direct parameter
ChildView(scrollOffset: scrollOffset)

修复 B:使用 @Observable 模型(如果许多视图都需要):

// Instead of storing primitive in environment:
@Observable class ScrollViewModel {
    var offset: CGFloat = 0
}

// Views depend on specific properties:
@Environment(ScrollViewModel.self) private var viewModel

var body: some View {
    Text("\(viewModel.offset)")  // Only updates when offset changes
}

验证:

  • 在 Instruments 中记录新的跟踪
  • 检查因果关系图——更新的视图应更少
  • 性能应有所提升(滚动/动画更流畅)

模式 D5:预览诊断(Xcode 26)

耗时:10 分钟

症状:预览无法加载,或因错误不明确而崩溃

适用情况:

  • 基本修复后预览仍然失败(swiftui-debugging 技能)
  • 错误消息不明确或过于笼统
  • 预览之前正常,突然停止工作

调查步骤:

1. 使用预览诊断按钮(2 分钟)

位置:编辑器菜单 → Canvas → Diagnostics

显示内容:

  • 详细的错误消息
  • 缺失的依赖项
  • 状态初始化问题
  • 预览专属问题
2. 检查崩溃日志(3 分钟)
# Open crash logs directory
open ~/Library/Logs/DiagnosticReports/

# Look for recent .crash files containing "Preview"
ls -lt ~/Library/Logs/DiagnosticReports/ | grep -i preview | head -5

查找内容:

  • 致命错误(数组越界、强制解包 nil)
  • 缺少模块导入
  • 框架初始化失败
3. 隔离问题(5 分钟)

创建最小预览:

// Start with empty preview
#Preview {
    Text("Test")
}

// If this works, gradually add:
#Preview {
    MyView()  // Your actual view, but with mock data
        .environment(MockModel())  // Provide all dependencies
}

// Find which dependency causes crash

常见问题:

错误原因修复方法
"Cannot find in scope"缺少依赖项将依赖项添加到预览中(见下方示例)
"Fatal error: Unexpectedly found nil"可选值解包失败在预览中提供非 nil 值
"No such module"缺少导入添加导入语句
静默崩溃(无错误)使用无效值初始化状态使用安全的默认值

修复模式:

// Missing @Environment
#Preview {
    ContentView()
        .environment(AppModel())  // Provide dependency
}

// Missing @EnvironmentObject (pre-iOS 17)
#Preview {
    ContentView()
        .environmentObject(AppModel())
}

// Missing ModelContainer (SwiftData)
#Preview {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(for: Item.self, configurations: config)

    return ContentView()
        .modelContainer(container)
}

// State with invalid defaults
@State var selectedIndex = 10  // ❌ Out of bounds
let items = ["a", "b", "c"]

// Fix: Safe default
@State var selectedIndex = 0  // ✅ Valid index

验证:

  • 预览加载时没有错误
  • 可以正常与预览交互
  • 更改会立即反映出来

生产环境危机情景

情况

背景:

  • iOS 26 版本在 2 天前发布
  • 用户报告“切换功能时设置屏幕会卡住”
  • 15% 的用户受到影响(通过 App Store 评论报告)
  • 副总裁每 2 小时就询问进展
  • 距离下一个部署窗口关闭还有 8 小时
  • 初级工程师建议:“让我试试改成 @ObservedObject”
危险信号——不要这么做

如果你在截止时间压力下听到以下任何说法,请停下来并使用诊断模式:

❌ “让我试试不同的属性包装器,看看哪个有效”

  • 随机更改等于猜测
  • 有 80% 的概率会让情况变得更糟

❌ “在我的设备上能正常运行,一定是 iOS 26 的问题”

  • 用户报告是真实的
  • 15% 说明这是系统性问题,不是边缘情况

❌ “如果修复无效,我们可以回滚”

  • App Store 审核需要 24 小时
  • 回滚不是即时的

❌ “添加 .id(UUID()) 来强制刷新”

  • 破坏状态保留
  • 掩盖根本原因

❌ “用户暂时会接受性能下降”

  • 发布后,你就要承担 24 小时的影响
  • 差评会一直存在
强制流程(不得走捷径)

总时间预算:90 分钟

阶段 1:复现(15 分钟)
# 1. Get exact steps from user report
# 2. Build Release mode
xcodebuild build -scheme YourApp -configuration Release

# 3. Test on device (not simulator)
# 4. Reproduce freeze 3+ times

如果无法复现:请受影响的用户提供视频录制或设备日志

阶段 2:使用模式 D2 诊断(30 分钟)
# Launch Instruments with SwiftUI template
# Command-I in Xcode

# Record while reproducing freeze
# Look for:
# - Long View Body Updates (red bars)
# - Cause & Effect Graph showing update cascade

查明:

  • 哪个视图开销较大?
  • 哪项数据更改触发了问题?
  • 更新了多少个视图?
阶段 3:应用针对性修复(20 分钟)

根据诊断结果:

如果是视图主体过长导致更新:

// Example finding: Formatter creation in body
// Fix: Move to cached formatter

如果是级联更新:

// Example finding: All toggle views reading entire settings array
// Fix: Per-toggle view models with granular dependencies

如果是环境问题:

// Example finding: Environment value updating every frame
// Fix: Remove from environment, use direct parameter
阶段 4:验证(15 分钟)
# Record new Instruments trace
# Compare before/after:
# - Long updates eliminated?
# - Update count reduced?
# - Freeze gone?

# Test on device 10+ times
阶段 5:提供证据并部署(10 分钟)
Slack to VP + team:

"Diagnostic complete: Settings screen freeze caused by formatter creation
in ToggleRow body (confirmed via SwiftUI Instrument, Long View Body Updates).

Each toggle tap recreated NumberFormatter + DateFormatter for all visible
toggles (20+ formatters per tap).

Fix: Cached formatters in SettingsViewModel, pre-formatted strings.
Verified: Settings screen now responds in <16ms (was 200ms+).

Deploying build 2.1.1 now. Will monitor for next 24 hours."

这表明:

  • 你基于证据进行了诊断,而不是猜测
  • 你了解根本原因
  • 你验证了修复
  • 你有把握地发布
时间成本对比
方案 A:猜测并祈祷
  • 尝试随机修复的时间:30 分钟
  • 部署时间:20 分钟
  • 确认修复失败所需时间:24 小时(等待下一次 App Store 审核)
  • 总延迟:24 小时以上
  • 用户受影响情况:问题持续到部署窗口结束
  • 风险:情况变得更糟,现在有两个 Bug
方案 B:诊断流程(本技能)
  • 诊断时间:45 分钟
  • 应用针对性修复的时间:20 分钟
  • 验证时间:15 分钟
  • 部署时间:10 分钟
  • 总耗时:90 分钟
  • 用户受影响情况:2 小时后问题解决
  • 把握:高(修复基于证据)

节省:22 小时,并避免情况恶化

确实有压力时

有时,管理者确实有理由要求加快速度。满足以下条件时,可以接受这种压力:

✅ 已完成诊断流程(90 分钟) ✅ 确切知道导致问题的视图/操作 ✅ 有针对性修复方案,而非猜测 ✅ 已在发布前通过 Instruments 验证 ✅ 发布时有证据支撑,而非抱着侥幸心理

记录你的决策(同上方的 Slack 模板)

应对催促的专业话术

如果有人催促你跳过诊断:

“我理解事情很紧急。跳过诊断意味着有 80% 的概率会发布错误的修复,让用户再多承受 24 小时的问题。诊断流程总共需要 90 分钟,并能让我们基于证据建立信心。我们会在不到 2 小时内完成修复部署和验证,而且不会有让情况变得更糟的风险。数据表明,诊断才是最快的解决途径。”


快速参考表

症状可能原因首要检查项模式修复时间
视图不更新缺少观察者 / 状态错误Self._printChanges()D110 min
视图更新过于频繁依赖范围过广Self._printChanges() → InstrumentsD1 → D230 min
状态重置身份发生变化.id() 修饰符、条件语句D315 min
级联更新环境问题环境修饰符D420 min
预览崩溃缺少依赖项 / 初始化错误Diagnostics 按钮D510 min
间歇性问题身份或时序问题重现 30+ 次D330 min
更新耗时过长(性能问题)body 操作开销过大Instruments(SwiftUI + Time Profiler)D230 min

决策框架

发布任何修复之前:

问题回答“是”?操作
你是否使用过 Self._printChanges()?否停止 — 使用模式 D1(5 min)
你是否运行过 SwiftUI Instrument?否停止 — 使用模式 D2(25 min)
你能否用一句话解释问题的原因?否停止 — 你只是在猜测
你是否已在 Instruments 中验证修复?否停止 — 发布前先测试
你是否检查过更简单的解释?否停止 — 查看诊断模式

五项全部回答“是” → 放心发布


常见错误

错误 1:“我添加了 @Observable,问题就解决了”

为什么不对:你不知道它为什么能解决问题

  • 现在可能有效,以后可能出问题
  • 可能掩盖了另一个 bug

正确做法:

  • 使用模式 D1(Self._printChanges())查看修改前的状态
  • 应用 @Observable
  • 再次使用模式 D1 查看修改后的状态
  • 确切了解发生了哪些变化
错误 2:“Instruments 太慢,不适合快速修复”

为什么不对:猜错时,猜测反而更费时间

  • 25 min 诊断 = 确定的修复方案
  • 5 min 猜测 × 3 次失败尝试 = 15 min + 问题仍未解决

正确做法:

  • 生产环境问题一律进行性能分析
  • 简单情况使用 Self._printChanges()
错误 3:“修复有效,我不需要验证”

为什么不对:手动测试 ≠ 验证

  • 可能只在你的特定测试中有效
  • 可能在边界情况下失败
  • 可能引入了性能回退

正确做法:

  • 修复后务必在 Instruments 中验证
  • 对比修复前后的跟踪记录
  • 测试边界情况(空数据、大量数据等)

快速命令参考

Instruments 命令
# Launch Instruments with SwiftUI template
# 1. In Xcode: Command-I
# 2. Or from command line:
open -a Instruments

# Build in Release mode (required for accurate profiling)
xcodebuild build -scheme YourScheme -configuration Release

# Clean derived data if needed
rm -rf ~/Library/Developer/Xcode/DerivedData
Self._printChanges() 调试模式
// Add temporarily to view body
var body: some View {
    let _ = Self._printChanges()  // Shows update reason

    // Your view code
}

请记住:提交前移除!

预览诊断
# Check preview crash logs
open ~/Library/Logs/DiagnosticReports/

# Filter for recent preview crashes
ls -lt ~/Library/Logs/DiagnosticReports/ | grep -i preview | head -5

# Xcode menu path:
# Editor → Canvas → Diagnostics
环境搜索
# Find environment modifiers
grep -r "\.environment(" --include="*.swift" .

# Find environment object usage
grep -r "@Environment" --include="*.swift" .

# Find view identity modifiers
grep -r "\.id(" --include="*.swift" .
Instruments 导航

在 Instruments 中(录制后):

  1. 选择 SwiftUI 跟踪轨道
  2. 展开以查看:
    • Update Groups 轨道
    • Long View Body Updates 轨道
    • Long Representable Updates 轨道
  3. 点击 Long View Body Updates 摘要
  4. 右键点击更新 → “Set Inspection Range and Zoom”
  5. 切换到 Time Profiler 轨道
  6. 在调用堆栈中找到你的视图(Command-F)

因果关系图:

  1. 在详细信息面板中展开层级结构
  2. 将鼠标悬停在视图名称上 → 点击箭头
  3. 选择 “Show Cause & Effect Graph”
  4. 点击节点以查看:
    • 状态变化节点 → 回溯信息
    • 视图 body 节点 → 依赖项

资源

WWDC:2025-306、2023-10160、2023-10149、2021-10022

文档:/xcode/understanding-hitches-in-your-app、/xcode/analyzing-hangs-in-your-app、/swiftui/managing-model-data-in-your-app

技能:axiom-swiftui-debugging、axiom-swiftui-performance、axiom-swiftui-layout、axiom-xcode-debugging

评论

支持 Markdown