协作头脑风暴
brainstorming
在实现前澄清需求、探索设计选择,并形成清晰的开发方向。
ios-debugger-agent【开发与工程】Builds an iOS app, installs and launches it on a simulator, then collects evidence about a runtime problem: unified logs, console output, screenshots, the accessibility tree, crash reports and an LLDB session. Works with Apple's command-line tools (xcodebuild, simctl, devicectl, lldb) and with Xcode's own MCP server when it is connected. Use when the…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Debugging an iOS app from an agent session means doing by command what a developer does with the Run button and the debug area: produce a simulator build, put it on a device, start it, watch what it prints, look at the screen, poke at the interface and stop in the debugger when needed. This skill gives the order of operations, the exact commands, and the shape of the report, so that every claim about the app ("the list is empty because the response failed to decode") is backed by something that was observed in this session.
Two control routes exist. Apple's command-line tools are always there and cover building, installing, launching, logs, screenshots, deep links and LLDB. Xcode's MCP server (xcrun mcpbridge) can add what the command line lacks, most importantly synthesized touches; use it when it is connected.
Ask only for what cannot be read from the repository: the symptom, the steps that trigger it, and whether a simulator the user already has open may be reused. Then look up the rest.
xcodebuild -version # Xcode in use
ls -d *.xcworkspace *.xcodeproj 2>/dev/null # a workspace wins over a project
xcodebuild -list -workspace Larkspur.xcworkspace # schemes
xcodebuild -showdestinations -workspace Larkspur.xcworkspace -scheme Larkspur
xcrun simctl list devices # names, UDIDs, (Booted) or (Shutdown)
Pick one simulator and refer to it by UDID from then on. The alias booted is convenient but picks an arbitrary device when several are running.
UDID=8F2C61B4-3D7A-4E0B-9C15-6A2E7D94B310 # iPhone 17, from the list above
| Situation | Route |
|---|---|
| The session lists tools from an Xcode MCP server | Use those tools for build, run, touch input and the debugger console; fall back to the commands below for anything missing |
| No such server, user works in Xcode | Offer to connect it (below), or continue with the command line |
| Headless machine, CI, or Xcode closed | Command line only |
Connecting Xcode's server is a one-time step the user approves: in Xcode, Settings, Intelligence, turn on "Allow external agents to use Xcode tools", keep the project open, then register the bridge.
claude mcp add --transport stdio xcode -- xcrun mcpbridge # Claude Code
codex mcp add xcode -- xcrun mcpbridge # Codex
The server arrived in Xcode 26.3. The Xcode 27 release notes say agents can boot simulators, install and launch apps, synthesize touch events and capture screenshots, and that the server gained tools to switch schemes and run destinations and to read the debugger console. The tools on offer depend on the Xcode version, so read the tool list the server reports instead of guessing a name, and fall back to the commands below for whatever is not in it.
Keep derived data inside the workspace folder so the product path is predictable and old builds from Xcode cannot be confused with this one.
xcodebuild build \
-workspace Larkspur.xcworkspace -scheme Larkspur -configuration Debug \
-destination "platform=iOS Simulator,id=$UDID" \
-derivedDataPath .build/DerivedData -quiet
-quiet prints only warnings and errors; a non-zero exit status means the build failed. On failure, quote the first error with its file and line, fix or report it, and do not continue to launch an older binary. Read the product location and bundle identifier from the build settings instead of assuming them:
xcodebuild -showBuildSettings \
-workspace Larkspur.xcworkspace -scheme Larkspur -configuration Debug \
-destination "platform=iOS Simulator,id=$UDID" -derivedDataPath .build/DerivedData \
| grep -E ' (TARGET_BUILD_DIR|WRAPPER_NAME|PRODUCT_BUNDLE_IDENTIFIER) = '
TARGET_BUILD_DIR joined with WRAPPER_NAME is the .app to install.
xcrun simctl boot "$UDID" # reports an error if it is already booted; that is fine
# the .app path is TARGET_BUILD_DIR/WRAPPER_NAME from step 3
xcrun simctl install "$UDID" .build/DerivedData/Build/Products/Debug-iphonesimulator/Larkspur.app
xcrun simctl launch --terminate-running-process "$UDID" app.larkspur.ios
launch prints the bundle identifier and the process ID. Useful variations:
--console-pty connects the app's standard output and error to the terminal and blocks until the app exits. Run it as a background command writing to a file, so print output and Swift runtime failures are captured.--stdout="$PWD/run/stdout.log" --stderr="$PWD/run/stderr.log" writes the streams to files without blocking. Give absolute paths; the help text does not say what a relative one resolves against.-hasSeenOnboarding YES overrides that UserDefaults key for this run only.--wait-for-debugger holds the process at start until LLDB attaches (step 7).A window does not have to be visible for the device to run or for screenshots to work.
Start log capture before reproducing, in the background, and note its PID so it can be stopped later.
mkdir -p run
xcrun simctl spawn "$UDID" log stream --level debug \
--predicate 'subsystem == "app.larkspur.ios"' > run/unified.log &
echo $! > run/log.pid
If the app does not use Logger with its own subsystem, filter by process instead: --predicate 'process == "Larkspur"'. For something that already happened, read history with xcrun simctl spawn "$UDID" log show --last 5m --info --debug and the same predicate; without those two flags show prints default-level messages only.
| Evidence | Command |
|---|---|
| What is on screen | xcrun simctl io "$UDID" screenshot run/01-after-launch.png, then open the image |
| A recording of a flow | xcrun simctl io "$UDID" recordVideo run/flow.mp4 in the background; stop it with an interrupt signal to its PID |
| Files, defaults, databases | xcrun simctl get_app_container "$UDID" app.larkspur.ios data prints the data container path |
| Crash reports | xcrun devicectl device info files --device "$UDID" --domain-type systemCrashLogs (Xcode 27), or the DiagnosticReports folder under the path from xcrun devicectl device info mountpoint --device "$UDID". On older Xcode a simulator app's crash report is a .ips file in the Mac's ~/Library/Logs/DiagnosticReports |
| Everything, for a bug report | xcrun simctl diagnose |
Take a screenshot after every step that should change the screen and look at it; do not infer the screen from logs.
simctl cannot tap. Use the first option that fits.
Xcode MCP touch tools when the server is connected and its tool list has them.
State changes that need no touch:
xcrun simctl openurl "$UDID" "larkspur://lists/weekly-shop" # deep link or universal link
xcrun simctl privacy "$UDID" grant photos app.larkspur.ios # also: revoke, reset
xcrun simctl ui "$UDID" appearance dark
xcrun simctl push "$UDID" app.larkspur.ios run/price-drop.apns # JSON payload with an "aps" key
xcrun simctl status_bar "$UDID" override --time "9:41" --batteryLevel 100
A temporary UI test when the project has a UI test target. XCUIAutomation finds elements by accessibility identifier or label, performs the gesture and can print the whole element tree.
// LarkspurUITests/DebugProbe.swift (delete when finished)
import XCTest
final class DebugProbe: XCTestCase {
@MainActor func testReproduce() {
let app = XCUIApplication()
app.launchArguments = ["-hasSeenOnboarding", "YES"]
app.launch()
app.buttons["addItemButton"].tap()
app.textFields["itemNameField"].tap()
app.textFields["itemNameField"].typeText("Oat milk")
app.buttons["Save"].tap()
XCTAssertTrue(app.staticTexts["Oat milk"].waitForExistence(timeout: 3))
print(app.debugDescription) // accessibility tree
add(XCTAttachment(screenshot: XCUIScreen.main.screenshot()))
}
}
xcodebuild test -workspace Larkspur.xcworkspace -scheme Larkspur \
-destination "platform=iOS Simulator,id=$UDID" -derivedDataPath .build/DerivedData \
-only-testing:LarkspurUITests/DebugProbe/testReproduce \
-resultBundlePath run/probe.xcresult
xcodebuild exits with an error when the result bundle path already exists, so delete run/probe.xcresult before a rerun.
Ask the user to perform the gesture while the log capture runs, when none of the above is available.
Simulator apps are processes on the Mac, so LLDB attaches by process ID.
xcrun simctl launch --wait-for-debugger "$UDID" app.larkspur.ios # prints the PID, e.g. 48213
xcrun lldb -p 48213
Inside LLDB: breakpoint set --file CartViewModel.swift --line 88, continue, then at the stop bt for the call stack, frame variable (alias v) for locals, p or po to evaluate an expression, and detach to leave the app running. For an app that hangs, one non-interactive command is enough:
xcrun lldb -p 48213 --batch -o 'thread backtrace all' -o 'detach' > run/threads.txt
For memory corruption or data races, rebuild with -enableAddressSanitizer YES or -enableThreadSanitizer YES on the xcodebuild line and reproduce again. Thread Sanitizer runs on simulators, not on physical iOS devices.
Stop background captures by PID (kill "$(cat run/log.pid)"), delete the probe test, and clear overrides (xcrun simctl status_bar "$UDID" clear). Leave the simulator booted unless the user asked otherwise. Report in this shape:
Build: Larkspur (Debug) for iPhone 17, iOS 26.2 — succeeded, 0 errors, 2 warnings
Launch: app.larkspur.ios, pid 48213
Steps: 1. launched 2. opened larkspur://lists/weekly-shop
Observed: screen shows the empty-state illustration (run/02-list.png)
Evidence: run/unified.log line 42 — "decode failed: keyNotFound(updatedAt)"
Cause: ListDTO requires updatedAt; the API omits it for lists never edited
Next: make updatedAt optional in ListDTO.swift:17 and rerun step 2
Request: "Run Larkspur on the simulator and find out why the weekly shop list is blank."
xcodebuild build -workspace Larkspur.xcworkspace -scheme Larkspur -configuration Debug \
-destination "platform=iOS Simulator,id=$UDID" -derivedDataPath .build/DerivedData -quiet
xcrun simctl boot "$UDID"
xcrun simctl install "$UDID" .build/DerivedData/Build/Products/Debug-iphonesimulator/Larkspur.app
mkdir -p run
xcrun simctl spawn "$UDID" log stream --level debug \
--predicate 'subsystem == "app.larkspur.ios"' > run/unified.log &
xcrun simctl launch --terminate-running-process "$UDID" app.larkspur.ios
xcrun simctl openurl "$UDID" "larkspur://lists/weekly-shop"
xcrun simctl io "$UDID" screenshot run/02-list.png
grep -n -i -B1 -E "error|failed" run/unified.log
Result:
41-2026-09-28 10:12:44.301577+0200 0x5f3a1 Default 0x0 48213 0 Larkspur: [app.larkspur.ios:network] GET /v2/lists/weekly-shop -> 200 (1.2 kB)
42:2026-09-28 10:12:44.318204+0200 0x5f3a1 Error 0x0 48213 0 Larkspur: [app.larkspur.ios:sync] decode failed: keyNotFound(CodingKeys(stringValue: "updatedAt"))
The request succeeded, so the network is not the problem; decoding threw and the view model fell back to its empty state. The report names ListDTO.updatedAt as the cause and attaches the screenshot.
Request: "The app dies on start since my last commit. What is it?"
xcrun simctl launch --console-pty --terminate-running-process "$UDID" app.larkspur.ios \
> run/console.log 2>&1
tail -n 5 run/console.log
Result:
app.larkspur.ios: 48977
Larkspur/AppEnvironment.swift:23: Fatal error: Unexpectedly found nil while unwrapping an Optional value
Line 23 force-unwraps Bundle.main.object(forInfoDictionaryKey: "LarkspurAPIBaseURL"). git diff HEAD~1 -- Larkspur/Info.plist shows the key was renamed to APIBaseURL in the last commit. The fix is to read the new key; after rebuilding, the same launch command prints only the PID and the screenshot shows the home screen.
simctl erase, simctl delete or simctl shutdown all on your own. They wipe or stop devices the user may depend on; ask first and name the device.booted with a UDID is how logs end up coming from a different simulator than the screenshots.--console-pty and log stream do not return. Run them in the background, write to files, and stop them by PID; never by process name, which could match the user's own tools.run/ and quote only the lines that matter.xcrun devicectl with a paired device (devicectl list devices, devicectl device install app, devicectl device process launch --console).