Letting Claude Code drive Xcode: the synchronized-groups trick
This app was later renamed Deep Cut Atlas. It’s called “Discoverer” throughout below, because that’s what it was called on the day this happened.
I’m building an iOS app with Claude Code doing most of the typing. The first real task was the least glamorous one: create the Xcode project and the app skeleton everything else hangs off. Three tabs, a folder structure, the MusicKit and CloudKit entitlements, a StoreKit config. Nothing hard on its own. The hard part is that Claude can’t click.
The problem: Claude can’t use the New Project wizard
An .xcodeproj isn’t a file you can write from scratch. It’s a bundle with a fussy project.pbxproj inside, and my own project rules say to never let an agent hand-edit that file. Xcode rewrites it constantly and one bad edit breaks the whole project. So the usual “Claude, create the project” doesn’t work, and “Claude, edit the pbxproj” is banned on purpose.
I looked at three ways to get past this:
- XcodeBuildMCP’s scaffold tool. It can generate a project from a template. But it produces a workspace plus a Swift package layout, which is more structure than a single app needs, and it splits the capabilities I cared about into a thin app target. More moving parts on day one.
- XcodeGen. Declarative project generation from a YAML spec. Reproducible, but it adds a build dependency and fights the “let Xcode own the project” rule.
- I create the shell, Claude fills it. I run the wizard once (about two minutes of clicking), Claude writes every Swift file after that.
I went with the third one. It’s the least clever and it matched my existing rules. I create the empty project, Claude does the other 95%.
The trick that makes it work: synchronized groups
Here’s the part I didn’t know would save me. Xcode 16 changed how new projects track files. Instead of recording every file by hand in the pbxproj, a new App project uses a file-system synchronized group (PBXFileSystemSynchronizedRootGroup if you go looking). The deal is simple: any file that appears in the target’s folder is automatically part of the target. No “Add Files to project” dialog, no pbxproj edit.
That’s exactly the seam Claude needs. It writes a .swift file into the folder and the file is in the build. I checked it was on before writing anything:
grep -c PBXFileSystemSynchronizedRootGroup Discoverer.xcodeproj/project.pbxproj
Three hits, good to go. From there Claude created the whole tree (App/, Features/, Models/, and so on) and wrote the views, the app entry point, and a placeholder model. First compile picked all of it up with zero project-file edits. That’s the thing I’ll remember from this task: on Xcode 16+, an agent can own the source files completely as long as it stays inside the synced folder.
The gotchas (there are always gotchas)
Synchronized groups bundle everything in the folder, which bit me twice.
First, I dropped a .gitkeep into two empty folders to keep them in git. The build died:
error: Multiple commands produce '.../Discoverer.app/.gitkeep'
Both .gitkeep files wanted to copy to the same spot in the app bundle. Fix: use a uniquely named, comment-only Swift file as the folder placeholder instead. It compiles to nothing and doesn’t get treated as a resource.
Then I named both placeholders Placeholder.swift. Different folders, same name. Build died again:
error: Multiple commands produce '.../Placeholder.stringsdata'
The compiler derives a per-file .stringsdata named after the source file, so file names have to be unique across the whole target, not just within a folder. Renaming them to ServicesPlaceholder.swift and UtilitiesPlaceholder.swift fixed it.
The deployment-target trap
New projects in Xcode 26 default the minimum deployment target to the current OS. Mine came out at iOS 26.5. When I tried to build for an iPhone 16 Pro simulator, xcodebuild told me no device matched. The reason: the newest simulator runtime I had installed was 26.2, which is below 26.5, so every simulator was ineligible. An app can’t run on an OS older than its own floor.
Dropping the target to iOS 17 fixed that. While I was there I hit a smaller trap. This command fails:
xcodebuild build -scheme Discoverer -destination 'platform=iOS Simulator,name=iPhone 16 Pro'
A bare device name means “latest OS”. The latest runtime on my machine was 26.x, which only ships iPhone 17 models. The iPhone 16 Pro lives on the 18.5 runtime. So I had to say which OS I meant:
xcodebuild build -scheme Discoverer -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.5' | xcbeautify
Keeping the shell launchable before CloudKit exists
I checked “Host in CloudKit” in the wizard, which adds the CloudKit entitlement but leaves the container identifier empty until you pick a team. A default SwiftData container with CloudKit turned on and no container can fail at launch, and I wanted a clean “it runs” before wiring iCloud. So I forced local storage for now:
let configuration = ModelConfiguration(cloudKitDatabase: .none)
modelContainer = try ModelContainer(for: Item.self, configurations: configuration)
The entitlement still declares CloudKit for later; the runtime just stays local until the container is real. One related rule I baked in from the start: every SwiftData property gets a default or is optional, because CloudKit-backed stores can’t enforce non-optional columns.
What I’d tell myself before starting
The mental model that worked: I own the project file and the GUI clicks, Claude owns the source. Xcode 16’s synchronized groups are what make that split clean instead of a constant stream of “add this file to the target” busywork. Check that the synced group is on, keep file names unique, set your deployment target to a version a simulator can actually run, and don’t turn on CloudKit at runtime until there’s a container behind it. The app builds, three tabs show up, and not one line of pbxproj was edited by hand.
Tooling note: build output goes through xcbeautify for readable logs, and XcodeBuildMCP is installed for later (its install is brew tap getsentry/xcodebuildmcp && brew install xcodebuildmcp, not the bare brew install you’ll see in stale notes).
Related reading
I reviewed my own iOS app with five parallel agents: here's what they found
Five ~700-line slices, briefs that ask what's well done too, and the verification step that separated real bugs from plausible ones.
Setting up Claude Code for an iOS project
Writing the CLAUDE.md for a new iOS app before a line of Swift exists - the MusicKit-in-simulator trap, CloudKit's SwiftData rules, and why the learning-project framing came first.
I only fixed the screenshot I was asked about, not the ones that were also broken
One recaptured marketing shot looked done - until the deflating question: is this really every screenshot? The other three in the same set were stale too, each in a different way.