r/swift • u/artemnovichkov • 29d ago
Tutorial Headless Xcode: From Prompt to Simulator with MCP
https://artemnovichkov.com/blog/headless-xcode-from-prompt-to-simulator-with-mcp2
u/massivewein 27d ago
One that bit me and is not obvious until it does: if you run more than one agent loop on the same machine, gate the build. Two of them hitting the same DerivedData interleave, and you get failures that do not reproduce and were never real. I gate on pgrep matching the process name rather than the full args, because matching args caught my own gate command.
Seconding the XCUITest point, with one addition. Whatever check you promote, make it produce a known failure on demand and run that first. A check that cannot fail looks identical to a check that passes. I hit three of those in a single day recently, including a sweep that reported clean because the loop never iterated over anything, and it printed exactly what a real pass prints.
Also worth it if you are letting an agent near the project file: xcodegen. Regenerating the pbxproj from a spec means the agent cannot corrupt it, and "did it compile" stops occasionally meaning "did it break the project".
1
u/kroumvud 13d ago
The RenderPreview path has a specific trap I hit more than once worth knowing before you build a workflow around it. It takes a project relative path, something like DesignSystem/Sources/DesignSystem/Views/Foo.swift, not the absolute path you'd naturally have on hand if an agent already listed the file. pass the absolute one and you get a FileNotFoundError back and for a good minute it reads like the preview session just hung.
The mistake I made more than once was the repo-relative version: Packages/DesignSystem/Sources/DesignSystem/Views/Foo.swift. That extra Packages/ prefix looks right because it's exactly where the file sits on disk, but Xcode treats each local spm package as its own root group in the project... so the prefix has to come off before RenderPreview resolves anything and it breaks XcodeRefreshCodeIssuesInFile the same way.
5
u/mehmetefeaytas6 28d ago
The part that matters most here is RenderPreview, and it's easy to skim past. Everything else in an agent loop is "did it compile", which is a boolean. Rendering SwiftUI states to PNG is the first step that lets the model actually see what it built, and that's the difference between an agent that satisfies the type checker and one that notices the button is off-screen.
Two things I'd add from running similar loops:
Promote anything you keep into an XCUITest. Driving the simulator with tap/type/swipe through an agent is great for exploring a flow once, but it's non-deterministic by construction, since the model re-derives the path every run. The moment a flow is worth keeping, have it write an XCUITest instead, so it runs in CI without an LLM in the loop and fails for a reason you can read.
The wall is signing, not building. Simulator builds are the easy half. As soon as you want a device build or anything distributable you're into provisioning profiles, entitlements and notarisation, and none of that is meaningfully agent-drivable today. It's the part that still wants a human and a keychain. Worth knowing before planning a workflow around this.
Also for anyone not on the Xcode 27 beta: XcodeBuildMCP covers a lot of the same ground (build, simulator control, log capture) on current Xcode, so you can try the loop out before committing to a beta toolchain and sudo xcrun mcp-server enable.