axiom-swiftui-debugging
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Debugging
SwiftUI 调试
Overview
概述
SwiftUI debugging falls into three categories, each with a different diagnostic approach:
- View Not Updating – You changed something but the view didn't redraw. Decision tree to identify whether it's struct mutation, lost binding identity, accidental view recreation, or missing observer pattern.
- Preview Crashes – Your preview won't compile or crashes immediately. Decision tree to distinguish between missing dependencies, state initialization failures, and Xcode cache corruption.
- Layout Issues – Views appearing in wrong positions, wrong sizes, overlapping unexpectedly. Quick reference patterns for common scenarios.
Core principle: Start with observable symptoms, test systematically, eliminate causes one by one. Don't guess.
Requires: Xcode 26+, iOS 17+ (iOS 14-16 patterns still valid, see notes)
Related skills: (cache corruption diagnosis), (observer patterns), (profiling with Instruments), (adaptive layout patterns)
axiom-xcode-debuggingaxiom-swift-concurrencyaxiom-swiftui-performanceaxiom-swiftui-layoutSwiftUI调试分为三类,每类对应不同的诊断方法:
- 视图未更新——你修改了内容但视图没有重绘。通过决策树判断是结构体突变、绑定标识丢失、意外视图重建还是缺少观察者模式。
- 预览崩溃——你的预览无法编译或立即崩溃。通过决策树区分是缺少依赖项、状态初始化失败还是Xcode缓存损坏。
- 布局问题——视图位置错误、尺寸不对或意外重叠。常见场景的快速参考模式。
核心原则:从可观察的症状入手,系统地进行测试,逐一排除原因。不要猜测。
要求:Xcode 26+、iOS 17+(iOS 14-16的模式仍然有效,详见说明)
相关技能:(缓存损坏诊断)、(观察者模式)、(使用Instruments进行性能分析)、(自适应布局模式)
axiom-xcode-debuggingaxiom-swift-concurrencyaxiom-swiftui-performanceaxiom-swiftui-layoutExample Prompts
示例提示
These are real questions developers ask that this skill is designed to answer:
以下是开发者实际会提出的问题,本技能正是为解答这些问题设计:
1. "My list item doesn't update when I tap the favorite button, even though the data changed"
1. "我点击收藏按钮后,列表项没有更新,尽管数据已经改变"
→ The skill walks through the decision tree to identify struct mutation vs lost binding vs missing observer
→ 本技能会引导你通过决策树判断是结构体突变、绑定丢失还是缺少观察者
2. "Preview crashes with 'Cannot find AppModel in scope' but it compiles fine"
2. "预览崩溃并提示'Cannot find AppModel in scope',但代码编译正常"
→ The skill shows how to provide missing dependencies with or
.environment().environmentObject()→ 本技能会展示如何通过或提供缺失的依赖项
.environment().environmentObject()3. "My counter resets to 0 every time I toggle a boolean, why?"
3. "每次我切换布尔值时,计数器都会重置为0,这是为什么?"
→ The skill identifies accidental view recreation from conditionals and shows fix
.opacity()→ 本技能会识别出条件语句导致的意外视图重建,并展示使用的修复方案
.opacity()4. "I'm using @Observable but the view still doesn't update when I change the property"
4. "我使用了@Observable,但修改属性后视图仍然不更新"
→ The skill explains when to use @State vs plain properties with @Observable objects
→ 本技能会解释何时使用@State,何时在@Observable对象中使用普通属性
5. "Text field loses focus when I start typing, very frustrating"
5. "我开始输入时,文本框失去焦点,非常令人沮丧"
→ The skill identifies ForEach identity issues and shows how to use stable IDs
→ 本技能会识别ForEach的标识问题,并展示如何使用稳定ID
When to Use SwiftUI Debugging
何时使用SwiftUI调试技能
Use this skill when
适合使用本技能的场景
- ✅ A view isn't updating when you expect it to
- ✅ Preview crashes or won't load
- ✅ Layout looks wrong on specific devices
- ✅ You're tempted to bandaid with @ObservedObject everywhere
- ✅ 视图未按预期更新
- ✅ 预览崩溃或无法加载
- ✅ 特定设备上布局显示异常
- ✅ 你忍不住想在所有地方用@ObservedObject来临时解决问题
Use axiom-xcode-debugging
instead when
axiom-xcode-debugging适合使用axiom-xcode-debugging
的场景
axiom-xcode-debugging- App crashes at runtime (not preview)
- Build fails completely
- You need environment diagnostics
- 应用在运行时崩溃(而非预览时)
- 构建完全失败
- 需要诊断环境问题
Use axiom-swift-concurrency
instead when
axiom-swift-concurrency适合使用axiom-swift-concurrency
的场景
axiom-swift-concurrency- Questions about async/await or MainActor
- Data race warnings
- 关于async/await或MainActor的问题
- 数据竞争警告
Debugging Tools
调试工具
Self._printChanges()
Self._printChanges()
SwiftUI provides a debug-only method to understand why a view's body was called.
Usage in LLDB:
swift
// Set breakpoint in view's body
// In LLDB console:
(lldb) expression Self._printChanges()Temporary in code (remove before shipping):
swift
var body: some View {
let _ = Self._printChanges() // Debug only
Text("Hello")
}Output interpretation:
MyView: @self changed
- Means the view value itself changed (parameters passed to view)
MyView: count changed
- Means @State property "count" triggered the update
MyView: (no output)
- Body not being called; view not updating at all⚠️ Important:
- Prefixed with underscore → May be removed in future releases
- NEVER submit to App Store with _printChanges calls
- Performance impact → Use only during debugging
When to use:
- Need to understand exact trigger for view update
- Investigating unexpected updates
- Verifying dependencies after refactoring
Cross-reference: For complex update patterns, use SwiftUI Instrument → see skill
axiom-swiftui-performanceSwiftUI提供了一个仅调试可用的方法,用于理解视图body被调用的原因。
在LLDB中的用法:
swift
// 在视图的body中设置断点
// 在LLDB控制台中:
(lldb) expression Self._printChanges()在代码中临时使用(发布前移除):
swift
var body: some View {
let _ = Self._printChanges() // 仅用于调试
Text("Hello")
}输出解读:
MyView: @self changed
- 表示视图本身的值发生了变化(传递给视图的参数)
MyView: count changed
- 表示@State属性"count"触发了更新
MyView: (无输出)
- Body未被调用;视图完全没有更新⚠️ 重要提示:
- 以下划线开头 → 未来版本可能会被移除
- 绝对不要将包含_printChanges调用的代码提交到App Store
- 会影响性能 → 仅在调试时使用
使用时机:
- 需要理解视图更新的确切触发因素
- 调查意外更新的原因
- 重构后验证依赖项
交叉参考:对于复杂的更新模式,请使用SwiftUI Instrument → 详见技能
axiom-swiftui-performanceView Not Updating Decision Tree
视图未更新决策树
The most common frustration: you changed @State but the view didn't redraw. The root cause is always one of four things.
最常见的困扰:你修改了@State但视图没有重绘。根本原因始终是以下四种之一。
Step 1: Can You Reproduce in a Minimal Preview?
步骤1:能否在最小化预览中复现问题?
swift
#Preview {
YourView()
}YES → The problem is in your code. Continue to Step 2.
NO → It's likely Xcode state or cache corruption. Skip to Preview Crashes section.
swift
#Preview {
YourView()
}能 → 问题出在你的代码中。继续步骤2。
不能 → 可能是Xcode状态或缓存损坏。跳转到预览崩溃部分。
Step 2: Diagnose the Root Cause
步骤2:诊断根本原因
Root Cause 1: Struct Mutation
根本原因1:结构体突变
Symptom: You modify a @State value directly, but the view doesn't update.
Why it happens: SwiftUI doesn't see direct mutations on structs. You need to reassign the entire value.
swift
// ❌ WRONG: Direct mutation doesn't trigger update
@State var items: [String] = []
func addItem(_ item: String) {
items.append(item) // SwiftUI doesn't see this change
}
// ✅ RIGHT: Reassignment triggers update
@State var items: [String] = []
func addItem(_ item: String) {
var newItems = items
newItems.append(item)
self.items = newItems // Full reassignment
}
// ✅ ALSO RIGHT: Use a binding
@State var items: [String] = []
var itemsBinding: Binding<[String]> {
Binding(
get: { items },
set: { items = $0 }
)
}Fix it: Always reassign the entire struct value, not pieces of it.
症状:你直接修改了@State值,但视图没有更新。
原因:SwiftUI无法检测到结构体的直接修改。你需要重新分配整个值。
swift
// ❌ 错误:直接修改不会触发更新
@State var items: [String] = []
func addItem(_ item: String) {
items.append(item) // SwiftUI无法检测到此变化
}
// ✅ 正确:重新分配触发更新
@State var items: [String] = []
func addItem(_ item: String) {
var newItems = items
newItems.append(item)
self.items = newItems // 完整重新分配
}
// ✅ 另一种正确方式:使用绑定
@State var items: [String] = []
var itemsBinding: Binding<[String]> {
Binding(
get: { items },
set: { items = $0 }
)
}修复方案:始终重新分配整个结构体值,而非修改其部分内容。
Root Cause 2: Lost Binding Identity
根本原因2:绑定标识丢失
Symptom: You pass a binding to a child view, but changes in the child don't update the parent.
Why it happens: You're passing or creating a new binding each time, breaking the two-way connection.
.constant()swift
// ❌ WRONG: Constant binding is read-only
@State var isOn = false
ToggleChild(value: .constant(isOn)) // Changes ignored
// ❌ WRONG: New binding created each render
@State var name = ""
TextField("Name", text: Binding(
get: { name },
set: { name = $0 }
)) // New binding object each time parent renders
// ✅ RIGHT: Pass the actual binding
@State var isOn = false
ToggleChild(value: $isOn)
// ✅ RIGHT (iOS 17+): Use @Bindable for @Observable objects
@Observable class Book {
var title = "Sample"
var isAvailable = true
}
struct EditView: View {
@Bindable var book: Book // Enables $book.title syntax
var body: some View {
TextField("Title", text: $book.title)
Toggle("Available", isOn: $book.isAvailable)
}
}
// ✅ ALSO RIGHT (iOS 17+): @Bindable as local variable
struct ListView: View {
@State private var books = [Book(), Book()]
var body: some View {
List(books) { book in
@Bindable var book = book // Inline binding
TextField("Title", text: $book.title)
}
}
}
// ✅ RIGHT (pre-iOS 17): Create binding once, not in body
@State var name = ""
@State var nameBinding: Binding<String>?
var body: some View {
if nameBinding == nil {
nameBinding = Binding(
get: { name },
set: { name = $0 }
)
}
return TextField("Name", text: nameBinding!)
}Fix it: Pass directly when possible. For @Observable objects (iOS 17+), use . If creating custom bindings (pre-iOS 17), create them in or cache them, not in .
$state@Bindableinitbody症状:你将绑定传递给子视图,但子视图的修改没有更新父视图。
原因:你传递的是或每次都创建新的绑定,破坏了双向连接。
.constant()swift
// ❌ 错误:常量绑定是只读的
@State var isOn = false
ToggleChild(value: .constant(isOn)) // 修改会被忽略
// ❌ 错误:每次渲染都创建新绑定
@State var name = ""
TextField("Name", text: Binding(
get: { name },
set: { name = $0 }
)) // 父视图每次渲染时都会创建新的绑定对象
// ✅ 正确:直接传递实际绑定
@State var isOn = false
ToggleChild(value: $isOn)
// ✅ 正确(iOS 17+):对@Observable对象使用@Bindable
@Observable class Book {
var title = "Sample"
var isAvailable = true
}
struct EditView: View {
@Bindable var book: Book // 启用$book.title语法
var body: some View {
TextField("Title", text: $book.title)
Toggle("Available", isOn: $book.isAvailable)
}
}
// ✅ 另一种正确方式(iOS 17+):@Bindable作为局部变量
struct ListView: View {
@State private var books = [Book(), Book()]
var body: some View {
List(books) { book in
@Bindable var book = book // 内联绑定
TextField("Title", text: $book.title)
}
}
}
// ✅ 正确(iOS 17之前):只创建一次绑定,不在body中创建
@State var name = ""
@State var nameBinding: Binding<String>?
var body: some View {
if nameBinding == nil {
nameBinding = Binding(
get: { name },
set: { name = $0 }
)
}
return TextField("Name", text: nameBinding!)
}修复方案:尽可能直接传递。对于@Observable对象(iOS 17+),使用。如果需要创建自定义绑定(iOS 17之前),在中创建或缓存它们,不要在中创建。
$state@BindableinitbodyRoot Cause 3: Accidental View Recreation
根本原因3:意外视图重建
Symptom: The view updates, but @State values reset to initial state. You see brief flashes of initial values.
Why it happens: The view got a new identity (removed from a conditional, moved in a container, or the container itself was recreated), causing SwiftUI to treat it as a new view.
swift
// ❌ WRONG: View identity changes when condition flips
@State var count = 0
var body: some View {
VStack {
if showCounter {
Counter() // Gets new identity each time showCounter changes
}
Button("Toggle") {
showCounter.toggle()
}
}
}
// Counter gets recreated, @State count resets to 0
// ✅ RIGHT: Preserve identity with opacity or hidden
@State var count = 0
var body: some View {
VStack {
Counter()
.opacity(showCounter ? 1 : 0)
Button("Toggle") {
showCounter.toggle()
}
}
}
// ✅ ALSO RIGHT: Use id() if you must conditionally show
@State var count = 0
var body: some View {
VStack {
if showCounter {
Counter()
.id("counter") // Stable identity
}
Button("Toggle") {
showCounter.toggle()
}
}
}Fix it: Preserve view identity by using instead of conditionals, or apply with a stable identifier.
.opacity().id()症状:视图更新了,但@State值重置为初始状态。你会看到初始值短暂闪现。
原因:视图获得了新的标识(从条件语句中移除、在容器中移动,或容器本身被重建),导致SwiftUI将其视为新视图。
swift
// ❌ 错误:条件切换时视图标识改变
@State var count = 0
var body: some View {
VStack {
if showCounter {
Counter() // 每次showCounter变化时,标识都会改变
}
Button("Toggle") {
showCounter.toggle()
}
}
}
// Counter会被重建,@State count重置为0
// ✅ 正确:使用opacity或hidden保留标识
@State var count = 0
var body: some View {
VStack {
Counter()
.opacity(showCounter ? 1 : 0)
Button("Toggle") {
showCounter.toggle()
}
}
}
// ✅ 另一种正确方式:如果必须条件显示,使用id()
@State var count = 0
var body: some View {
VStack {
if showCounter {
Counter()
.id("counter") // 稳定标识
}
Button("Toggle") {
showCounter.toggle()
}
}
}修复方案:通过使用而非条件语句来保留视图标识,或使用设置稳定标识符。
.opacity().id()Root Cause 4: Missing Observer Pattern
根本原因4:缺少观察者模式
Symptom: An object changed, but views observing it didn't update.
Why it happens: SwiftUI doesn't know to watch for changes in the object.
swift
// ❌ WRONG: Property changes don't trigger update
class Model {
var count = 0 // Not observable
}
struct ContentView: View {
let model = Model() // New instance each render, not observable
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1 // View doesn't update
}
}
}
// ✅ RIGHT (iOS 17+): Use @Observable with @State
@Observable class Model {
var count = 0 // No @Published needed
}
struct ContentView: View {
@State private var model = Model() // @State, not @StateObject
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1 // View updates
}
}
}
// ✅ RIGHT (iOS 17+): Injected @Observable objects
struct ContentView: View {
var model: Model // Just a plain property
var body: some View {
Text("\(model.count)") // View updates when count changes
}
}
// ✅ RIGHT (iOS 17+): @Observable with environment
@Observable class AppModel {
var count = 0
}
@main
struct MyApp: App {
@State private var model = AppModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(model) // Add to environment
}
}
}
struct ContentView: View {
@Environment(AppModel.self) private var model // Read from environment
var body: some View {
Text("\(model.count)")
}
}
// ✅ RIGHT (pre-iOS 17): Use @StateObject/ObservableObject
class Model: ObservableObject {
@Published var count = 0
}
struct ContentView: View {
@StateObject var model = Model() // For owned instances
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1 // View updates
}
}
}
// ✅ RIGHT (pre-iOS 17): Use @ObservedObject for injected instances
struct ContentView: View {
@ObservedObject var model: Model // Passed in from parent
var body: some View {
Text("\(model.count)")
}
}Fix it (iOS 17+): Use macro on your class, then to store it. Views automatically track dependencies on properties they read.
@Observable@StateFix it (pre-iOS 17): Use if you own the object, if it's injected, or if it's shared across the tree.
@StateObject@ObservedObject@EnvironmentObjectWhy @Observable is better (iOS 17+):
- Automatic dependency tracking (only reads trigger updates)
- No wrapper needed
@Published - Works with instead of
@State@StateObject - Can pass as plain property instead of
@ObservedObject
See also: Managing model data in your app
症状:对象发生了变化,但观察它的视图没有更新。
原因:SwiftUI不知道要监听该对象的变化。
swift
// ❌ 错误:属性变化不会触发更新
class Model {
var count = 0 // 不可观察
}
struct ContentView: View {
let model = Model() // 每次渲染都会创建新实例,且不可观察
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1 // 视图不会更新
}
}
}
// ✅ 正确(iOS 17+):将@Observable与@State结合使用
@Observable class Model {
var count = 0 // 不需要@Published
}
struct ContentView: View {
@State private var model = Model() // @State,而非@StateObject
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1 // 视图会更新
}
}
}
// ✅ 正确(iOS 17+):注入@Observable对象
struct ContentView: View {
var model: Model // 只是普通属性
var body: some View {
Text("\(model.count)") // 当count变化时,视图会更新
}
}
// ✅ 正确(iOS 17+):@Observable与环境结合使用
@Observable class AppModel {
var count = 0
}
@main
struct MyApp: App {
@State private var model = AppModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(model) // 添加到环境中
}
}
}
struct ContentView: View {
@Environment(AppModel.self) private var model // 从环境中读取
var body: some View {
Text("\(model.count)")
}
}
// ✅ 正确(iOS 17之前):使用@StateObject/ObservableObject
class Model: ObservableObject {
@Published var count = 0
}
struct ContentView: View {
@StateObject var model = Model() // 用于拥有的实例
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1 // 视图会更新
}
}
}
// ✅ 正确(iOS 17之前):对注入的实例使用@ObservedObject
struct ContentView: View {
@ObservedObject var model: Model // 从父视图传入
var body: some View {
Text("\(model.count)")
}
}修复方案(iOS 17+):在类上使用宏,然后使用存储它。视图会自动跟踪其读取的属性的依赖关系。
@Observable@State修复方案(iOS 17之前):如果是你拥有的对象,使用;如果是注入的对象,使用;如果是在整个视图树中共享的对象,使用。
@StateObject@ObservedObject@EnvironmentObject为什么@Observable更好(iOS 17+):
- 自动依赖跟踪(只有读取操作会触发更新)
- 不需要@Published包装器
- 与@State一起使用,而非@StateObject
- 可以作为普通属性传递,而非@ObservedObject
另请参阅:在应用中管理模型数据
Decision Tree Summary
决策树总结
View not updating?
├─ Can reproduce in preview?
│ ├─ YES: Problem is in code
│ │ ├─ Modified struct directly? → Struct Mutation
│ │ ├─ Passed binding to child? → Lost Binding Identity
│ │ ├─ View inside conditional? → Accidental Recreation
│ │ └─ Object changed but view didn't? → Missing Observer
│ └─ NO: Likely cache/Xcode state → See Preview Crashes视图未更新?
├─ 能否在预览中复现?
│ ├─ 能:问题出在代码中
│ │ ├─ 是否直接修改了结构体? → 结构体突变
│ │ ├─ 是否向子视图传递了绑定? → 绑定标识丢失
│ │ ├─ 视图是否在条件语句中? → 意外重建
│ │ └─ 对象变化但视图未更新? → 缺少观察者
│ └─ 不能:可能是缓存/Xcode状态问题 → 查看预览崩溃部分Preview Crashes Decision Tree
预览崩溃决策树
When your preview won't load or crashes immediately, the three root causes are distinct.
当你的预览无法加载或立即崩溃时,根本原因分为三类。
Step 1: What's the Error?
步骤1:错误类型是什么?
Error Type 1: "Cannot find in scope" or "No such module"
错误类型1:"Cannot find in scope"或"No such module"
Root cause: Preview missing a required dependency (@EnvironmentObject, @Environment, imported module).
swift
// ❌ WRONG: ContentView needs a model, preview doesn't provide it
struct ContentView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text(model.title)
}
}
#Preview {
ContentView() // Crashes: model not found
}
// ✅ RIGHT: Provide the dependency
#Preview {
ContentView()
.environmentObject(AppModel())
}
// ✅ ALSO RIGHT: Check for missing imports
// If using custom types, make sure they're imported in preview file
#Preview {
MyCustomView() // Make sure MyCustomView is defined or imported
}Fix it: Trace the error, find what's missing, provide it to the preview.
根本原因:预览缺少必需的依赖项(@EnvironmentObject、@Environment、导入的模块)。
swift
// ❌ 错误:ContentView需要model,但预览未提供
struct ContentView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text(model.title)
}
}
#Preview {
ContentView() // 崩溃:找不到model
}
// ✅ 正确:提供依赖项
#Preview {
ContentView()
.environmentObject(AppModel())
}
// ✅ 另一种正确方式:检查是否缺少导入
// 如果使用自定义类型,确保在预览文件中导入了它们
#Preview {
MyCustomView() // 确保MyCustomView已定义或已导入
}修复方案:跟踪错误,找到缺失的内容,并在预览中提供它。
Error Type 2: Fatal error or Silent crash (no error message)
错误类型2:致命错误或无声崩溃(无错误消息)
Root cause: State initialization failed at runtime. The view tried to access data that doesn't exist.
swift
// ❌ WRONG: Index out of bounds at runtime
struct ListView: View {
@State var selectedIndex = 10
let items = ["a", "b", "c"]
var body: some View {
Text(items[selectedIndex]) // Crashes: index 10 doesn't exist
}
}
// ❌ WRONG: Optional forced unwrap fails
struct DetailView: View {
@State var data: Data?
var body: some View {
Text(data!.title) // Crashes if data is nil
}
}
// ✅ RIGHT: Safe defaults
struct ListView: View {
@State var selectedIndex = 0 // Valid index
let items = ["a", "b", "c"]
var body: some View {
if selectedIndex < items.count {
Text(items[selectedIndex])
}
}
}
// ✅ RIGHT: Handle optionals
struct DetailView: View {
@State var data: Data?
var body: some View {
if let data = data {
Text(data.title)
} else {
Text("No data")
}
}
}Fix it: Review your @State initializers. Check array bounds, optional unwraps, and default values.
根本原因:运行时状态初始化失败。视图尝试访问不存在的数据。
swift
// ❌ 错误:运行时索引越界
struct ListView: View {
@State var selectedIndex = 10
let items = ["a", "b", "c"]
var body: some View {
Text(items[selectedIndex]) // 崩溃:索引10不存在
}
}
// ❌ 错误:可选类型强制解包失败
struct DetailView: View {
@State var data: Data?
var body: some View {
Text(data!.title) // 如果data为nil,会崩溃
}
}
// ✅ 正确:使用安全默认值
struct ListView: View {
@State var selectedIndex = 0 // 有效索引
let items = ["a", "b", "c"]
var body: some View {
if selectedIndex < items.count {
Text(items[selectedIndex])
}
}
}
// ✅ 正确:处理可选类型
struct DetailView: View {
@State var data: Data?
var body: some View {
if let data = data {
Text(data.title)
} else {
Text("无数据")
}
}
}修复方案:检查你的@State初始化器。检查数组边界、可选类型解包和默认值。
Error Type 3: Works fine locally but preview won't load
错误类型3:本地运行正常但预览无法加载
Root cause: Xcode cache corruption. The preview process has stale information about your code.
Diagnostic checklist:
- Preview worked yesterday, code hasn't changed → Likely cache
- Restarting Xcode fixes it temporarily but returns → Definitely cache
- Same code builds in simulator fine but preview fails → Cache
- Multiple unrelated previews fail at once → Cache
Fix it (in order):
- Restart Preview Canvas:
Cmd+Option+P - Restart Xcode completely (File → Close Window, then reopen project)
- Nuke derived data:
rm -rf ~/Library/Developer/Xcode/DerivedData - Rebuild:
Cmd+B
If still broken after all four steps: It's not cache, see Error Types 1 or 2.
根本原因:Xcode缓存损坏。预览进程持有代码的过时信息。
诊断清单:
- 预览昨天还能正常工作,代码没有变化 → 很可能是缓存问题
- 重启Xcode后暂时修复,但问题再次出现 → 肯定是缓存问题
- 相同代码在模拟器中能正常构建,但预览失败 → 缓存问题
- 多个不相关的预览同时失败 → 缓存问题
修复方案(按顺序执行):
- 重启预览画布:
Cmd+Option+P - 完全重启Xcode(文件→关闭窗口,然后重新打开项目)
- 删除派生数据:
rm -rf ~/Library/Developer/Xcode/DerivedData - 重新构建:
Cmd+B
如果执行完这四个步骤后仍然有问题:不是缓存问题,请查看错误类型1或2。
Decision Tree Summary
决策树总结
Preview crashes?
├─ Error message visible?
│ ├─ "Cannot find in scope" → Missing Dependency
│ ├─ "Fatal error" or silent crash → State Init Failure
│ └─ No error → Likely Cache Corruption
└─ Try: Restart Preview → Restart Xcode → Nuke DerivedData预览崩溃?
├─ 可见错误消息?
│ ├─ "Cannot find in scope" → 缺少依赖项
│ ├─ "Fatal error"或无声崩溃 → 状态初始化失败
│ └─ 无错误 → 很可能是缓存损坏
└─ 尝试:重启预览 → 重启Xcode → 删除派生数据Layout Issues Quick Reference
布局问题快速参考
Layout problems are usually visually obvious. Match your symptom to the pattern.
布局问题通常在视觉上很明显。将你的症状与对应的模式匹配。
Pattern 1: Views Overlapping in ZStack
模式1:ZStack中视图重叠
Symptom: Views stacked on top of each other, some invisible.
Root cause: Z-order is wrong or you're not controlling visibility.
swift
// ❌ WRONG: Can't see the blue view
ZStack {
Rectangle().fill(.blue)
Rectangle().fill(.red)
}
// ✅ RIGHT: Use zIndex to control layer order
ZStack {
Rectangle().fill(.blue).zIndex(0)
Rectangle().fill(.red).zIndex(1)
}
// ✅ ALSO RIGHT: Hide instead of removing from hierarchy
ZStack {
Rectangle().fill(.blue)
Rectangle().fill(.red).opacity(0.5)
}症状:视图堆叠在一起,部分视图不可见。
根本原因:Z轴顺序错误或未控制可见性。
swift
// ❌ 错误:看不到蓝色视图
ZStack {
Rectangle().fill(.blue)
Rectangle().fill(.red)
}
// ✅ 正确:使用zIndex控制层顺序
ZStack {
Rectangle().fill(.blue).zIndex(0)
Rectangle().fill(.red).zIndex(1)
}
// ✅ 另一种正确方式:隐藏而非从层级中移除
ZStack {
Rectangle().fill(.blue)
Rectangle().fill(.red).opacity(0.5)
}Pattern 2: GeometryReader Sizing Weirdness
模式2:GeometryReader尺寸异常
Symptom: View is tiny or taking up the entire screen unexpectedly.
Root cause: GeometryReader sizes itself to available space; parent doesn't constrain it.
swift
// ❌ WRONG: GeometryReader expands to fill all available space
VStack {
GeometryReader { geo in
Text("Size: \(geo.size)")
}
Button("Next") { }
}
// Text takes entire remaining space
// ✅ RIGHT: Constrain the geometry reader
VStack {
GeometryReader { geo in
Text("Size: \(geo.size)")
}
.frame(height: 100)
Button("Next") { }
}症状:视图异常小或意外占据整个屏幕。
根本原因:GeometryReader会自适应可用空间,而父视图没有对其进行约束。
swift
// ❌ 错误:GeometryReader会扩展以填充所有可用空间
VStack {
GeometryReader { geo in
Text("Size: \(geo.size)")
}
Button("Next") { }
}
// 文本占据所有剩余空间
// ✅ 正确:约束GeometryReader
VStack {
GeometryReader { geo in
Text("Size: \(geo.size)")
}
.frame(height: 100)
Button("Next") { }
}Pattern 3: SafeArea Complications
模式3:SafeArea问题
Symptom: Content hidden behind notch, or not using full screen space.
Root cause: applied to wrong view.
.ignoresSafeArea()swift
// ❌ WRONG: Only the background ignores safe area
ZStack {
Color.blue.ignoresSafeArea()
VStack {
Text("Still respects safe area")
}
}
// ✅ RIGHT: Container ignores, children position themselves
ZStack {
Color.blue
VStack {
Text("Can now use full space")
}
}
.ignoresSafeArea()
// ✅ ALSO RIGHT: Be selective about which edges
ZStack {
Color.blue
VStack { ... }
}
.ignoresSafeArea(edges: .horizontal) // Only horizontal症状:内容被刘海遮挡,或未使用全屏空间。
根本原因:应用到了错误的视图上。
.ignoresSafeArea()swift
// ❌ 错误:只有背景忽略安全区域
ZStack {
Color.blue.ignoresSafeArea()
VStack {
Text("仍然遵循安全区域")
}
}
// ✅ 正确:容器忽略安全区域,子视图自行定位
ZStack {
Color.blue
VStack {
Text("现在可以使用全屏空间")
}
}
.ignoresSafeArea()
// ✅ 另一种正确方式:选择性忽略特定边缘
ZStack {
Color.blue
VStack { ... }
}
.ignoresSafeArea(edges: .horizontal) // 仅忽略水平方向Pattern 4: frame() vs fixedSize() Confusion
模式4:frame()与fixedSize()混淆
Symptom: Text truncated, buttons larger than text, sizing behavior unpredictable.
Root cause: Mixing (constrains) with (expands to content).
frame()fixedSize()swift
// ❌ WRONG: fixedSize() overrides frame()
Text("Long text here")
.frame(width: 100)
.fixedSize() // Overrides the frame constraint
// ✅ RIGHT: Use frame() to constrain
Text("Long text here")
.frame(width: 100, alignment: .leading)
.lineLimit(1)
// ✅ RIGHT: Use fixedSize() only for natural sizing
VStack(spacing: 0) {
Text("Small")
.fixedSize() // Sizes to text
Text("Large")
.fixedSize()
}症状:文本被截断、按钮比文本大、尺寸行为不可预测。
根本原因:混合使用(约束)和(自适应内容)。
frame()fixedSize()swift
// ❌ 错误:fixedSize()覆盖了frame()
Text("Long text here")
.frame(width: 100)
.fixedSize() // 覆盖了frame约束
// ✅ 正确:使用frame()进行约束
Text("Long text here")
.frame(width: 100, alignment: .leading)
.lineLimit(1)
// ✅ 正确:仅对自然尺寸使用fixedSize()
VStack(spacing: 0) {
Text("Small")
.fixedSize() // 尺寸自适应文本
Text("Large")
.fixedSize()
}Pattern 5: Modifier Order Matters
模式5:修饰符顺序很重要
Symptom: Padding, corners, or shadows appearing in wrong place.
Root cause: Applying modifiers in wrong order. SwiftUI applies bottom-to-top.
swift
// ❌ WRONG: Corners applied after padding
Text("Hello")
.padding()
.cornerRadius(8) // Corners are too large
// ✅ RIGHT: Corners first, then padding
Text("Hello")
.cornerRadius(8)
.padding()
// ❌ WRONG: Shadow after frame
Text("Hello")
.frame(width: 100)
.shadow(radius: 4) // Shadow only on frame bounds
// ✅ RIGHT: Shadow includes all content
Text("Hello")
.shadow(radius: 4)
.frame(width: 100)症状:内边距、圆角或阴影位置错误。
根本原因:修饰符的应用顺序错误。SwiftUI从下到上应用修饰符。
swift
// ❌ 错误:在添加内边距后应用圆角
Text("Hello")
.padding()
.cornerRadius(8) // 圆角过大
// ✅ 正确:先应用圆角,再添加内边距
Text("Hello")
.cornerRadius(8)
.padding()
// ❌ 错误:在frame之后应用阴影
Text("Hello")
.frame(width: 100)
.shadow(radius: 4) // 阴影仅在frame边界上
// ✅ 正确:阴影包含所有内容
Text("Hello")
.shadow(radius: 4)
.frame(width: 100)View Identity
视图标识
Understanding View Identity
理解视图标识
SwiftUI uses view identity to track views over time, preserve state, and animate transitions. Understanding identity is critical for debugging state preservation and animation issues.
SwiftUI使用视图标识来跟踪视图的变化、保留状态并处理动画过渡。理解标识对于调试状态保留和动画问题至关重要。
Two Types of Identity
两种标识类型
1. Structural Identity (Implicit)
1. 结构标识(隐式)
Position in view hierarchy determines identity:
swift
VStack {
Text("First") // Identity: VStack.child[0]
Text("Second") // Identity: VStack.child[1]
}When structural identity changes:
swift
if showDetails {
DetailView() // Identity changes when condition changes
SummaryView()
} else {
SummaryView() // Same type, different position = different identity
}Problem: gets recreated each time, losing @State values.
SummaryView视图在层级中的位置决定了标识:
swift
VStack {
Text("First") // 标识:VStack.child[0]
Text("Second") // 标识:VStack.child[1]
}结构标识变化的场景:
swift
if showDetails {
DetailView() // 条件变化时标识改变
SummaryView()
} else {
SummaryView() // 类型相同,但位置不同 = 不同标识
}问题:每次都会被重建,丢失@State值。
SummaryView2. Explicit Identity
2. 显式标识
You control identity with modifier:
.id()swift
DetailView()
.id(item.id) // Explicit identity tied to item
// When item.id changes → SwiftUI treats as different view
// → @State resets
// → Animates transition你可以使用修饰符控制标识:
.id()swift
DetailView()
.id(item.id) // 显式标识与item绑定
// 当item.id变化时 → SwiftUI将其视为不同的视图
// → @State重置
// → 触发过渡动画Common Identity Issues
常见标识问题
Issue 1: State Resets Unexpectedly
问题1:状态意外重置
Symptom: @State values reset to initial values when you don't expect.
Cause: View identity changed (position in hierarchy or .id() value changed).
swift
// ❌ PROBLEM: Identity changes when showDetails toggles
@State private var count = 0
var body: some View {
VStack {
if showDetails {
CounterView(count: $count) // Position changes
}
Button("Toggle") {
showDetails.toggle()
}
}
}
// ✅ FIX: Stable identity with .opacity()
var body: some View {
VStack {
CounterView(count: $count)
.opacity(showDetails ? 1 : 0) // Same identity always
Button("Toggle") {
showDetails.toggle()
}
}
}
// ✅ ALSO FIX: Explicit stable ID
var body: some View {
VStack {
if showDetails {
CounterView(count: $count)
.id("counter") // Stable ID
}
Button("Toggle") {
showDetails.toggle()
}
}
}症状:@State值在你不期望的时候重置为初始值。
原因:视图标识发生了变化(在层级中的位置或.id()值改变)。
swift
// ❌ 问题:showDetails切换时标识变化
@State private var count = 0
var body: some View {
VStack {
if showDetails {
CounterView(count: $count) // 位置变化
}
Button("Toggle") {
showDetails.toggle()
}
}
}
// ✅ 修复:使用.opacity()保持稳定标识
var body: some View {
VStack {
CounterView(count: $count)
.opacity(showDetails ? 1 : 0) // 始终保持相同标识
Button("Toggle") {
showDetails.toggle()
}
}
}
// ✅ 另一种修复:使用显式稳定ID
var body: some View {
VStack {
if showDetails {
CounterView(count: $count)
.id("counter") // 稳定ID
}
Button("Toggle") {
showDetails.toggle()
}
}
}Issue 2: Animations Don't Work
问题2:动画不工作
Symptom: View changes but doesn't animate.
Cause: Identity changed, SwiftUI treats as remove + add instead of update.
swift
// ❌ PROBLEM: Identity changes with selection
ForEach(items) { item in
ItemView(item: item)
.id(item.id + "-\(selectedID)") // ID changes when selection changes
}
// ✅ FIX: Stable identity
ForEach(items) { item in
ItemView(item: item, isSelected: item.id == selectedID)
.id(item.id) // Stable ID
}症状:视图变化但没有动画效果。
原因:标识变化,SwiftUI将其视为移除+添加,而非更新。
swift
// ❌ 问题:选择变化时标识改变
ForEach(items) { item in
ItemView(item: item)
.id(item.id + "-\(selectedID)") // 选择变化时ID改变
}
// ✅ 修复:使用稳定标识
ForEach(items) { item in
ItemView(item: item, isSelected: item.id == selectedID)
.id(item.id) // 稳定ID
}Issue 3: ForEach with Changing Data
问题3:ForEach项跳动
Symptom: List items jump around or animate incorrectly.
Cause: Non-unique or changing identifiers.
swift
// ❌ WRONG: Index-based ID changes when array changes
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
Text(item.name)
}
// ❌ WRONG: Non-unique IDs
ForEach(items, id: \.category) { item in // Multiple items per category
Text(item.name)
}
// ✅ RIGHT: Stable, unique IDs
ForEach(items, id: \.id) { item in
Text(item.name)
}
// ✅ RIGHT: Make type Identifiable
struct Item: Identifiable {
let id = UUID()
var name: String
}
ForEach(items) { item in // id: \.id implicit
Text(item.name)
}症状:List项位置跳动或动画异常。
原因:非唯一或变化的标识符。
swift
// ❌ 错误:基于索引的ID在数组变化时会改变
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
Text(item.name)
}
// ❌ 错误:非唯一ID
ForEach(items, id: \.category) { item in // 每个分类有多个项
Text(item.name)
}
// ✅ 正确:稳定、唯一的ID
ForEach(items, id: \.id) { item in
Text(item.name)
}
// ✅ 正确:让类型遵循Identifiable
struct Item: Identifiable {
let id = UUID()
var name: String
}
ForEach(items) { item in // 隐式使用id: \.id
Text(item.name)
}When to Use .id()
何时使用.id()
Use .id() to:
- Force view recreation when data changes fundamentally
- Animate transitions between distinct states
- Reset @State when external dependency changes
Example: Force recreation on data change:
swift
DetailView(item: item)
.id(item.id) // New item → new view → @State resetsDon't use .id() when:
- You just need to update view content (use bindings instead)
- Trying to fix update issues (investigate root cause instead)
- Identity is already stable
使用.id()的场景:
- 当数据发生根本性变化时,强制视图重建
- 在不同状态之间触发过渡动画
- 当外部依赖变化时重置@State
示例:数据变化时强制重建:
swift
DetailView(item: item)
.id(item.id) // 新item → 新视图 → @State重置不要使用.id()的场景:
- 你只需要更新视图内容(使用绑定代替)
- 试图修复更新问题(先调查根本原因)
- 标识已经稳定
Debugging Identity Issues
调试标识问题
1. Self._printChanges()
1. Self._printChanges()
swift
var body: some View {
let _ = Self._printChanges()
// Check if "@self changed" appears when you don't expect
}swift
var body: some View {
let _ = Self._printChanges()
// 检查是否在不期望的时候出现"@self changed"
}2. Check .id() modifiers
2. 检查.id()修饰符
Search codebase for - are IDs changing unexpectedly?
.id()在代码库中搜索——ID是否意外变化?
.id()3. Check conditionals
3. 检查条件语句
Views in change position → different identity.
if/elseFix: Use or stable instead.
.opacity().id()if/else修复:使用或稳定的代替。
.opacity().id()Identity Quick Reference
标识快速参考
| Symptom | Likely Cause | Fix |
|---|---|---|
| State resets | Identity change | Use |
| No animation | Identity change | Remove |
| ForEach jumps | Non-unique ID | Use unique, stable IDs |
| Unexpected recreation | Conditional position | Add explicit |
See also: WWDC21: Demystify SwiftUI
| 症状 | 可能原因 | 修复方案 |
|---|---|---|
| 状态重置 | 标识变化 | 使用 |
| 无动画 | 标识变化 | 移除 |
| ForEach项跳动 | 非唯一ID | 使用唯一、稳定的ID |
| 意外重建 | 条件位置变化 | 添加显式 |
另请参阅:WWDC21: 揭秘SwiftUI
Pressure Scenarios and Real-World Constraints
压力场景与实际约束
When you're under deadline pressure, you'll be tempted to shortcuts that hide problems instead of fixing them.
当你面临截止日期压力时,你可能会想走捷径,隐藏问题而非修复它们。
Scenario 1: "Preview keeps crashing, we ship tomorrow"
场景1:"预览一直崩溃,我们明天就要发布"
Red flags you might hear
你可能听到的危险言论
- "Just rebuild everything"
- "Delete derived data and don't worry about it"
- "Ship without validating in preview"
- "It works on my machine, good enough"
The danger: You skip diagnosis, cache issue recurs after 2 weeks in production, you're debugging while users hit crashes.
What to do instead (5-minute protocol, total):
- Restart Preview Canvas: (30 seconds)
Cmd+Option+P - Restart Xcode (2 minutes)
- Nuke derived data: (30 seconds)
rm -rf ~/Library/Developer/Xcode/DerivedData - Rebuild: (2 minutes)
Cmd+B - Still broken? Use the dependency or initialization decision trees above
Time cost: 5 minutes diagnosis + 2 minutes fix = 7 minutes total
Cost of skipping: 30 min shipping + 24 hours debug cycle = 24+ hours total
- "重新构建所有内容"
- "删除派生数据,别管它"
- "不验证预览就发布"
- "在我机器上能运行,足够了"
危险:你跳过了诊断,缓存问题会在发布2周后再次出现,你需要在用户遇到崩溃时进行调试。
正确做法(总共5分钟):
- 重启预览画布:(30秒)
Cmd+Option+P - 完全重启Xcode(文件→关闭窗口,然后重新打开项目)(2分钟)
- 删除派生数据:(30秒)
rm -rf ~/Library/Developer/Xcode/DerivedData - 重新构建:(2分钟)
Cmd+B - 仍然有问题?使用上面的依赖项或初始化决策树
时间成本:5分钟诊断 + 2分钟修复 = 总共7分钟
跳过诊断的成本:30分钟发布 + 24小时调试周期 = 总共24小时以上
Scenario 2: "View won't update, let me just wrap it in @ObservedObject"
场景2:"视图不更新,我直接用@ObservedObject包起来吧"
Red flags you might think
你可能会有的危险想法
- "Adding @ObservedObject everywhere will fix it"
- "Use ObservableObject as a band-aid"
- "Add @Published to random properties"
- "It's probably a binding issue, I'll just create a custom binding"
The danger: You're treating symptoms, not diagnosing. Same view won't update in other contexts. You've just hidden the bug.
What to do instead (2-minute diagnosis):
- Can you reproduce in a minimal preview? If NO → cache corruption (see Scenario 1)
- If YES: Test each root cause in order:
- Does the view have @State that you're modifying directly? → Struct Mutation
- Did the view move into a conditional recently? → View Recreation
- Are you passing bindings to children that have changed? → Lost Binding Identity
- Only if none of above: Missing Observer
- Fix the actual root cause, not with @ObservedObject band-aid
Decision principle: If you can't name the specific root cause, you haven't diagnosed yet. Don't code until you can answer "the problem is struct mutation because...".
- "在所有地方添加@ObservedObject就能解决问题"
- "用ObservableObject临时解决"
- "给随机属性添加@Published"
- "可能是绑定问题,我创建一个自定义绑定试试"
危险:你在治疗症状,而非诊断根本原因。同一个视图在其他上下文中仍然不会更新。你只是隐藏了bug。
正确做法(2分钟诊断):
- 能否在最小化预览中复现?如果不能 → 缓存损坏(见场景1)
- 如果能:按顺序测试每个根本原因:
- 视图是否有你直接修改的@State? → 结构体突变
- 视图最近是否被移到条件语句中? → 视图重建
- 你是否向子视图传递了变化的绑定? → 绑定标识丢失
- 只有以上都不是时:缺少观察者
- 修复实际的根本原因,而非用@ObservedObject临时解决
决策原则:如果你无法说出具体的根本原因,说明你还没有诊断完成。在你能回答"问题是结构体突变,因为..."之前,不要编写代码。
Scenario 2b: "Intermittent updates - it works sometimes, not always"
场景2b:"间歇性更新——有时有效,有时无效"
Red flags you might think
你可能会有的危险想法
- "It must be a threading issue, let me add @MainActor everywhere"
- "Let me try @ObservedObject, @State, and custom Binding until something works"
- "Delete DerivedData and hope cache corruption fixes it"
- "This is unfixable, let me ship without this feature"
The danger: You're exhausted after 2 hours of guessing. You're 17 hours from App Store submission. You're panicking. Every minute feels urgent, so you stop diagnosing and start flailing.
Intermittent bugs are the MOST important to diagnose correctly. One wrong guess now creates a new bug. You ship with a broken view AND a new bug. App Store rejects you. You miss launch.
What to do instead (60-minute systematic diagnosis):
Step 1: Reproduce in preview (15 min)
- Create minimal preview of just the broken view
- Tap/interact 20 times
- Does it fail intermittently, consistently, or never?
- Fails in preview: Real bug in your code, use decision tree above
- Works in preview but fails in app: Cache or environment issue, use Preview Crashes decision tree
- Can't reproduce at all: Intermittent race condition, investigate further
Step 2: Isolate the variable (15 min)
- If it's intermittent in preview: Likely view recreation
- Did the view recently move into a conditional? Remove it and test
- Did you add logic that might recreate the parent? Remove it and test
if
- If it works in preview but fails in app: Likely environment/cache issue
- Try on different device/simulator
- Try after clearing DerivedData
Step 3: Apply the specific fix (30 min)
- Once you've identified view recreation: Use instead of conditionals
.opacity() - Once you've identified struct mutation: Use full reassignment
- Once you've verified it's cache: Nuke DerivedData properly
Step 4: Verify 100% reliability (until submission)
- Run the same interaction 30+ times
- Test on multiple devices/simulators
- Get QA to verify
- Only ship when it's 100% reproducible (not the bug, the FIX)
Time cost: 60 minutes diagnosis + 30 minutes fix + confidence = submit at 9am
Cost of guessing: 2 hours already + 3 more hours guessing + new bug introduced + crash reports post-launch + emergency patch + reputation damage = miss launch + post-launch chaos
The decision principle: Intermittent bugs require SYSTEMATIC diagnosis. The slower you go in diagnosis, the faster you get to the fix. Guessing is the fastest way to disaster.
- "肯定是线程问题,我在所有地方添加@MainActor"
- "我试试@ObservedObject、@State和自定义绑定,直到某个能工作"
- "删除派生数据,希望缓存损坏能解决问题"
- "这个问题无法修复,我发布时去掉这个功能"
危险:你在2小时的猜测后筋疲力尽。距离App Store提交还有17小时。你很恐慌。每一分钟都很紧迫,所以你停止诊断,开始乱试。
间歇性bug是最需要正确诊断的。现在的一个错误猜测会引入新的bug。你发布了一个有问题的视图,还引入了新bug。App Store拒绝了你的提交。你错过了发布时间。
正确做法(60分钟系统诊断):
步骤1:在预览中复现(15分钟)
- 创建仅包含有问题视图的最小化预览
- 点击/交互20次
- 它是间歇性失败、持续失败还是从未失败?
- 在预览中失败:代码中存在真实bug,使用上面的决策树
- 在预览中工作但在应用中失败:缓存或环境问题,使用预览崩溃决策树
- 完全无法复现:间歇性竞争条件,进一步调查
步骤2:隔离变量(15分钟)
- 如果在预览中间歇性失败:可能是视图重建
- 视图最近是否被移到条件语句中?移除它并测试
- 你是否添加了可能重建父视图的逻辑?移除它并测试
if
- 如果在预览中工作但在应用中失败:可能是环境/缓存问题
- 在不同设备/模拟器上尝试
- 清除派生数据后尝试
步骤3:应用特定修复(30分钟)
- 一旦确定是视图重建:使用代替条件语句
.opacity() - 一旦确定是结构体突变:使用完整重新分配
- 一旦确定是缓存问题:正确删除派生数据
步骤4:验证100%可靠性(直到提交)
- 重复相同的交互30次以上
- 在多个设备/模拟器上测试
- 让QA验证
- 只有当修复100%有效时才发布(不是bug能复现,而是修复能解决)
时间成本:60分钟诊断 + 30分钟修复 + 信心 = 上午9点提交
猜测的成本:已经2小时 + 再3小时猜测 + 引入新bug + 发布后崩溃报告 + 紧急补丁 + 声誉损失 = 错过发布 + 发布后混乱
决策原则:间歇性bug需要系统诊断。诊断时越慢,你找到修复方案的速度就越快。猜测是最快走向灾难的方式。
Professional script for co-leads who suggest guessing
对建议你猜测的同事的专业回复
"I appreciate the suggestion. Adding @ObservedObject everywhere is treating the symptom, not the root cause. The skill says intermittent bugs create NEW bugs when we guess. I need 60 minutes for systematic diagnosis. If I can't find the root cause by then, we'll disable the feature and ship a clean v1.1. The math shows we have time—I can complete diagnosis, fix, AND verification before the deadline."
"我感谢你的建议。在所有地方添加@ObservedObject是在治疗症状,而非根本原因。本技能指出,间歇性bug在我们猜测时会引入新bug。我需要60分钟进行系统诊断。如果到那时我还找不到根本原因,我们会禁用这个功能,发布干净的v1.1。计算显示我们有时间——我可以在截止日期前完成诊断、修复和验证。"
Scenario 3: "Layout looks wrong on iPad, we're out of time"
场景3:"iPad上布局显示错误,我们没时间了"
Red flags you might think
你可能会有的危险想法
- "Add some padding and magic numbers"
- "It's probably a safe area thing, let me just ignore it"
- "Let's lock this to iPhone only"
- "GeometryReader will solve this"
The danger: Magic numbers break on other sizes. SafeArea ignoring is often wrong. Locking to iPhone means you ship a broken iPad experience.
What to do instead (3-minute diagnosis):
- Run in simulator or device
- Use Debug View Hierarchy: Debug menu → View Hierarchy (takes 30 seconds to load)
- Check: Is the problem SafeArea, ZStack ordering, or GeometryReader sizing?
- Use the correct pattern from the Quick Reference above
Time cost: 3 minutes diagnosis + 5 minutes fix = 8 minutes total
Cost of magic numbers: Ship wrong, report 2 weeks later, debug 4 hours, patch in update = 2+ weeks delay
- "添加一些内边距和魔法数字"
- "可能是安全区域问题,我直接忽略它"
- "我们把它锁定为仅iPhone可用"
- "GeometryReader能解决这个问题"
危险:魔法数字在其他尺寸上会失效。忽略安全区域通常是错误的。锁定为仅iPhone意味着你发布了一个有问题的iPad体验。
正确做法(3分钟诊断):
- 在模拟器或设备上运行
- 使用调试视图层级:调试菜单 → 视图层级(加载需要30秒)
- 检查:问题是SafeArea、ZStack顺序还是GeometryReader尺寸?
- 使用上面快速参考中的正确模式
时间成本:3分钟诊断 + 5分钟修复 = 总共8分钟
使用魔法数字的成本:发布错误,2周后收到报告,调试4小时,在更新中修复 = 2周以上的延迟
Quick Reference
快速参考
Common View Update Fixes
常见视图更新修复方案
swift
// Fix 1: Reassign the full struct
@State var items: [String] = []
var newItems = items
newItems.append("new")
self.items = newItems
// Fix 2: Pass binding correctly
@State var value = ""
ChildView(text: $value) // Pass binding, not value
// Fix 3: Preserve view identity
View().opacity(isVisible ? 1 : 0) // Not: if isVisible { View() }
// Fix 4: Observe the object
@StateObject var model = MyModel()
@ObservedObject var model: MyModelswift
// 修复1:重新分配完整结构体
@State var items: [String] = []
var newItems = items
newItems.append("new")
self.items = newItems
// 修复2:正确传递绑定
@State var value = ""
ChildView(text: $value) // 传递绑定,而非值
// 修复3:保留视图标识
View().opacity(isVisible ? 1 : 0) // 不要用:if isVisible { View() }
// 修复4:观察对象
@StateObject var model = MyModel()
@ObservedObject var model: MyModelCommon Preview Fixes
常见预览修复方案
swift
// Fix 1: Provide dependencies
#Preview {
ContentView()
.environmentObject(AppModel())
}
// Fix 2: Safe defaults
@State var index = 0 // Not 10, if array has 3 items
// Fix 3: Nuke cache
// Terminal: rm -rf ~/Library/Developer/Xcode/DerivedDataswift
// 修复1:提供依赖项
#Preview {
ContentView()
.environmentObject(AppModel())
}
// 修复2:安全默认值
@State var index = 0 // 不要用10,如果数组只有3个项
// 修复3:清除缓存
// 终端:rm -rf ~/Library/Developer/Xcode/DerivedDataCommon Layout Fixes
常见布局修复方案
swift
// Fix 1: Z-order
Rectangle().zIndex(1)
// Fix 2: Constrain GeometryReader
GeometryReader { geo in ... }.frame(height: 100)
// Fix 3: SafeArea
ZStack { ... }.ignoresSafeArea()
// Fix 4: Modifier order
Text().cornerRadius(8).padding() // Corners firstswift
// 修复1:Z轴顺序
Rectangle().zIndex(1)
// 修复2:约束GeometryReader
GeometryReader { geo in ... }.frame(height: 100)
// 修复3:安全区域
ZStack { ... }.ignoresSafeArea()
// 修复4:修饰符顺序
Text().cornerRadius(8).padding() // 先圆角Real-World Examples
实际示例
Example 1: List Item Doesn't Update When Tapped
示例1:点击后列表项不更新
Scenario: You have a list of tasks. When you tap a task to mark it complete, the checkmark should appear, but it doesn't.
Code:
swift
struct TaskListView: View {
@State var tasks: [Task] = [...]
var body: some View {
List {
ForEach(tasks, id: \.id) { task in
HStack {
Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
Text(task.title)
Spacer()
Button("Done") {
// ❌ WRONG: Direct mutation
task.isComplete.toggle()
}
}
}
}
}
}Diagnosis using the skill:
- Can you reproduce in preview? YES
- Are you modifying the struct directly? YES → Struct Mutation (Root Cause 1)
Fix:
swift
Button("Done") {
// ✅ RIGHT: Full reassignment
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index].isComplete.toggle()
}
}Why this works: SwiftUI detects the array reassignment, triggering a redraw. The task in the List updates.
场景:你有一个任务列表。当你点击任务标记为完成时,应该显示对勾,但没有显示。
代码:
swift
struct TaskListView: View {
@State var tasks: [Task] = [...]
var body: some View {
List {
ForEach(tasks, id: \.id) { task in
HStack {
Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
Text(task.title)
Spacer()
Button("Done") {
// ❌ 错误:直接修改
task.isComplete.toggle()
}
}
}
}
}
}使用本技能诊断:
- 能否在预览中复现?能
- 是否直接修改了结构体?能 → 结构体突变(根本原因1)
修复方案:
swift
Button("Done") {
// ✅ 正确:完整重新分配
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index].isComplete.toggle()
}
}为什么有效:SwiftUI检测到数组重新分配,触发重绘。列表中的任务会更新。
Example 2: Preview Crashes with "No Such Module"
示例2:预览崩溃并提示"No Such Module"
Scenario: You created a custom data model. It works fine in the app, but the preview crashes with "Cannot find 'CustomModel' in scope".
Code:
swift
import SwiftUI
// ❌ WRONG: Preview missing the dependency
#Preview {
TaskDetailView(task: Task(...))
}
struct TaskDetailView: View {
@Environment(\.modelContext) var modelContext
let task: Task // Custom model
var body: some View {
Text(task.title)
}
}Diagnosis using the skill:
- What's the error? "Cannot find in scope" → Missing Dependency (Error Type 1)
- What does TaskDetailView need? The Task model and modelContext
Fix:
swift
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Task.self, configurations: config)
return TaskDetailView(task: Task(title: "Sample"))
.modelContainer(container)
}Why this works: Providing the environment object and model container satisfies the view's dependencies. Preview loads successfully.
场景:你创建了一个自定义数据模型。它在应用中工作正常,但预览崩溃并提示"Cannot find 'CustomModel' in scope"。
代码:
swift
import SwiftUI
// ❌ 错误:预览缺少依赖项
#Preview {
TaskDetailView(task: Task(...))
}
struct TaskDetailView: View {
@Environment(\.modelContext) var modelContext
let task: Task // 自定义模型
var body: some View {
Text(task.title)
}
}使用本技能诊断:
- 错误类型是什么?"Cannot find in scope" → 缺少依赖项(错误类型1)
- TaskDetailView需要什么?Task模型和modelContext
修复方案:
swift
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Task.self, configurations: config)
return TaskDetailView(task: Task(title: "Sample"))
.modelContainer(container)
}为什么有效:提供环境对象和模型容器满足了视图的依赖项。预览成功加载。
Example 3: Text Field Value Changes Don't Appear
示例3:文本框值变化不显示
Scenario: You have a search field. You type characters, but the text doesn't appear in the UI. However, the search results DO update.
Code:
swift
struct SearchView: View {
@State var searchText = ""
var body: some View {
VStack {
// ❌ WRONG: Passing constant binding
TextField("Search", text: .constant(searchText))
Text("Results for: \(searchText)") // This updates
List {
ForEach(results(for: searchText), id: \.self) { result in
Text(result)
}
}
}
}
func results(for text: String) -> [String] {
// Returns filtered results
}
}Diagnosis using the skill:
- Can you reproduce in preview? YES
- Are you passing a binding to a child view? YES (TextField)
- Is it a constant binding? YES → Lost Binding Identity (Root Cause 2)
Fix:
swift
// ✅ RIGHT: Pass the actual binding
TextField("Search", text: $searchText)Why this works: passes a two-way binding. TextField writes changes back to @State, triggering a redraw. Text field now shows typed characters.
$searchText场景:你有一个搜索框。你输入字符,但UI中不显示文本。然而,搜索结果会更新。
代码:
swift
struct SearchView: View {
@State var searchText = ""
var body: some View {
VStack {
// ❌ 错误:传递常量绑定
TextField("Search", text: .constant(searchText))
Text("Results for: \(searchText)") // 这个会更新
List {
ForEach(results(for: searchText), id: \.self) { result in
Text(result)
}
}
}
}
func results(for text: String) -> [String] {
// 返回过滤后的结果
}
}使用本技能诊断:
- 能否在预览中复现?能
- 是否向子视图传递了绑定?能(TextField)
- 是否是常量绑定?能 → 绑定标识丢失(根本原因2)
修复方案:
swift
// ✅ 正确:传递实际绑定
TextField("Search", text: $searchText)为什么有效:传递了双向绑定。TextField将变化写回@State,触发重绘。文本框现在会显示输入的字符。
$searchTextSimulator Verification
模拟器验证
After fixing SwiftUI issues, verify with visual confirmation in the simulator.
修复SwiftUI问题后,在模拟器中进行视觉确认验证。
Why Simulator Verification Matters
为什么模拟器验证很重要
SwiftUI previews don't always match simulator behavior:
- Different rendering — Some visual effects only work on device/simulator
- Different timing — Animations may behave differently
- Different state — Full app lifecycle vs isolated preview
Use simulator verification for:
- Layout fixes (spacing, alignment, sizing)
- View update fixes (state changes, bindings)
- Animation and gesture issues
- Before/after visual comparison
SwiftUI预览并不总是与模拟器行为匹配:
- 渲染不同 — 一些视觉效果仅在设备/模拟器上有效
- 时序不同 — 动画行为可能不同
- 状态不同 — 完整应用生命周期 vs 孤立预览
适合使用模拟器验证的场景:
- 布局修复(间距、对齐、尺寸)
- 视图更新修复(状态变化、绑定)
- 动画和手势问题
- 修复前后的视觉对比
Quick Verification Workflow
快速验证工作流
bash
undefinedbash
undefined1. Take "before" screenshot
1. 截取"修复前"截图
/axiom:screenshot
/axiom:screenshot
2. Apply your fix
2. 应用你的修复
3. Rebuild and relaunch
3. 重新构建并重新启动
xcodebuild build -scheme YourScheme
xcodebuild build -scheme YourScheme
4. Take "after" screenshot
4. 截取"修复后"截图
/axiom:screenshot
/axiom:screenshot
5. Compare screenshots to verify fix
5. 对比截图以验证修复
undefinedundefinedNavigating to Problem Screens
导航到问题屏幕
If the bug is deep in your app, use debug deep links to navigate directly:
bash
undefined如果bug在应用深处,使用调试深度链接直接导航:
bash
undefined1. Add debug deep links (see deep-link-debugging skill)
1. 添加调试深度链接(详见deep-link-debugging技能)
Example: debug://settings, debug://recipe-detail?id=123
示例:debug://settings, debug://recipe-detail?id=123
2. Navigate and capture
2. 导航并捕获
xcrun simctl openurl booted "debug://problem-screen"
sleep 1
/axiom:screenshot
undefinedxcrun simctl openurl booted "debug://problem-screen"
sleep 1
/axiom:screenshot
undefinedFull Simulator Testing
完整模拟器测试
For complex scenarios (state setup, multiple steps, log analysis):
bash
/axiom:test-simulatorThen describe what you want to test:
- "Navigate to the recipe editor and verify the layout fix"
- "Test the profile screen with empty state"
- "Verify the animation doesn't stutter anymore"
对于复杂场景(状态设置、多步骤、日志分析):
bash
/axiom:test-simulator然后描述你要测试的内容:
- "导航到食谱编辑器并验证布局修复"
- "测试空状态下的个人资料屏幕"
- "验证动画不再卡顿"
Before/After Example
修复前后示例
Before fix (view not updating):
bash
undefined修复前(视图未更新):
bash
undefined1. Reproduce bug
1. 复现bug
xcrun simctl openurl booted "debug://recipe-list"
sleep 1
xcrun simctl io booted screenshot /tmp/before-fix.png
xcrun simctl openurl booted "debug://recipe-list"
sleep 1
xcrun simctl io booted screenshot /tmp/before-fix.png
Screenshot shows: Tapping star doesn't update UI
截图显示:点击星标后UI不更新
**After fix** (added @State binding):
```bash
**修复后**(添加了@State绑定):
```bash2. Test fix
2. 测试修复
xcrun simctl openurl booted "debug://recipe-list"
sleep 1
xcrun simctl io booted screenshot /tmp/after-fix.png
xcrun simctl openurl booted "debug://recipe-list"
sleep 1
xcrun simctl io booted screenshot /tmp/after-fix.png
Screenshot shows: Star updates immediately when tapped
截图显示:点击星标后立即更新UI
**Time saved**: 60%+ faster iteration with visual verification vs manual navigation
---
**节省的时间**:与手动导航相比,视觉验证的迭代速度快60%以上
---Resources
资源
WWDC: 2025-256, 2025-306, 2023-10160, 2023-10149, 2021-10022
Docs: /swiftui/managing-model-data-in-your-app, /swiftui, /swiftui/state-and-data-flow, /xcode/previews, /observation
Skills: axiom-swiftui-performance, axiom-swiftui-debugging-diag, axiom-xcode-debugging, axiom-swift-concurrency
WWDC:2025-256, 2025-306, 2023-10160, 2023-10149, 2021-10022
文档:/swiftui/managing-model-data-in-your-app, /swiftui, /swiftui/state-and-data-flow, /xcode/previews, /observation
技能:axiom-swiftui-performance, axiom-swiftui-debugging-diag, axiom-xcode-debugging, axiom-swift-concurrency