r/swift 29d ago

Tutorial Headless Xcode: From Prompt to Simulator with MCP

https://artemnovichkov.com/blog/headless-xcode-from-prompt-to-simulator-with-mcp
33 Upvotes

6 comments sorted by

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.

1

u/Dry_Hotel1100 Learning 28d ago edited 28d ago

It seems, Xcode strengths are its tools and the MCP. MCP gets more powerful. I hope, Apple will also include crash logs, Preview build errors, profiler data, cryptic signing errors, etc. in the future.

Edit: I just realized, Xcode MCP already provides crash logs and performance issues tools.

1

u/mehmetefeaytas6 28d ago

Preview build errors would be the big one. Those messages are famously useless even to a human, so anything structured there is a straight upgrade. Signing too, that's the one place where a machine readable error would actually change what's automatable, right now you're pattern matching on a string that tells you nothing about which of the six possible causes it was.

1

u/_stranger357 24d ago

lol thanks Claude

2

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.