I’ve been waiting for Xcode 27.1, hoping to test how my apps adapt in the iPhone Duo simulator. Amusingly, 27.1 never arrived — Apple shipped 27.2 beta first instead. The iPhone Duo simulator is still missing, but the new project.xcproj caught my eye. What it replaces is project.pbxproj, the build graph inside the .xcodeproj bundle, not the bundle itself. What’s good about it, what does it fix, and what stays the same? This article takes a look.
xcodeproj and pbxproj
Xcode creates an .xcodeproj file for every project. What is it made of, and what job does it do?
.xcodeproj looks like a file but is actually a bundle. It stores how Xcode understands the project: which targets exist, how files enter the build, what settings compile them, along with some of your working state — breakpoints, folding, cursor position, windows, and the files you last had open.
A typical single-project bundle looks roughly like this:
MyApp.xcodeproj/
├── project.pbxproj # project description (can be project.xcproj as of Xcode 27.2)
├── project.xcworkspace/ # even a single project carries an implicit workspace
│ ├── contents.xcworkspacedata
│ ├── xcshareddata/
│ └── xcuserdata/
├── xcshareddata/
│ ├── xcschemes/ # shared schemes: Run / Test / Profile / Archive
│ └── swiftpm/
│ └── Package.resolved # resolved SPM dependency versions
└── xcuserdata/
└── <username>.xcuserdatad/
├── xcdebugger/
│ └── Breakpoints_v2.xcbkptlist # breakpoints for this project
├── xcschemes/ # schemes only you can see
└── UserInterfaceState.xcuserstate # folding, cursor, windows, last open files
Source code, .xcconfig files, and Info.plist all live outside this bundle, and xcuserdata is usually added to .gitignore in practice.
The critical piece inside the bundle is project.pbxproj (which, as of Xcode 27.2, can also be project.xcproj). It is the file that actually describes the project’s build relationships.
pbxproj: A Declarative Structure With a Period Feel
project.pbxproj is the build graph of an Xcode project. It covers which targets the project has; the file tree shown in the project navigator; how files enter the build (Compile Sources, Copy Bundle Resources, and so on); Debug / Release configurations and build settings (which can be externalized to .xcconfig); dependencies; and project-level metadata.
Its format dates back to the NeXTSTEP era. It isn’t XML plist, and it isn’t JSON — it’s the OpenStep / ASCII plist NeXT left behind: braces, semicolons, isa, and a pile of 24-character hexadecimal IDs. More than thirty years on, a typical pbxproj still looks like this:
// !$*UTF8*$!
{
archiveVersion = 1;
objectVersion = 90;
objects = {
A1B2C3D4E5F6789012345678 /* ContentView.swift */ = {
isa = PBXFileReference;
lastKnownFileType = sourcecode.swift;
path = ContentView.swift;
sourceTree = "<group>";
};
...
};
rootObject = 000000000000000000000000 /* Project object */;
}
objects is one enormous flat dictionary. Files, groups, targets, build phases, build files, configuration lists — all laid out side by side keyed by ID, referring to one another through those IDs.
This structure is deeply unfriendly to collaboration, and tends to become the hardest text in the repository to merge. The reason is simple: it is both the shared truth and an encoding of that truth in a form that resists sharing. One very small shared change can leave traces in many non-adjacent places.
On top of that, some configuration is stored in ordered arrays whose order usually carries no meaning for compilation. Two people each add a file; even though the operations don’t conflict at all, both may touch the same stretch of list, and Git reports a conflict. It looks like “the project broke,” when in reality two unrelated files were simply inserted into the same collection.
Xcode’s recent push toward Folders is one way of easing this. The old Group writes every file into pbxproj; the Folder introduced in Xcode 16 mainly records the directory relationship and stays in sync with what’s on disk when the project opens, so adding and removing source files day to day barely touches the project file. Switch your source directories to Folders and both the diffs and the conflicts shrink noticeably.
In the age of agents, the drawbacks of pbxproj are amplified further. A great many IDs carry both object identity and cross-references, so an agent modifying a project usually has to locate the right object in a flat table, then update several mutually referencing places in step, all while trying to keep the serialized output faithful to Xcode’s conventions. The time goes not into understanding the project model, but into imitating Xcode’s text format.
What Changes With xcproj
As a replacement for project.pbxproj, Apple’s positioning of project.xcproj is restrained: make diffs easier to understand, reduce merge conflicts, and keep migration cost as low as possible. Same project model, different expression. It is not a new project DSL, nor a replacement for project manifests like Tuist’s Project.swift.
In other words, project.xcproj changes nothing fundamental about the .xcodeproj bundle; it only swaps out how the build graph inside is expressed. Schemes, Package.resolved, and xcuserdata all fall outside the scope of this change. Existing projects are not converted automatically, and Xcode 27.2 supports both formats.
New projects created in Xcode 27.2 already use project.xcproj by default. Existing projects can choose JSON in File inspector → Project Format, or convert from the command line:
xcodebuild -project MyApp.xcodeproj -convert-project "Xcode Project"
That said, xcproj is not a straight transliteration of pbxproj into JSON. It reshapes how the project model is expressed:
- From a flat ID table to a tree that mirrors the UI: instead of dumping everything into
objects, it unfolds into structures likefiles/targets/build-settings, so diffs look closer to the operations you actually performed in the interface; - The direction of relationships is reversed: a file can declare which targets it belongs to via
target-membership, so adding a file no longer means editing several mutually referencing places at once; - IDs haven’t disappeared, but cross-references lean far more on names and paths, and everyday diffs are no longer flooded with unreadable IDs.
The difference is easy to see. That same ContentView.swift requires four separate entries in pbxproj — a file reference, a build file, the group’s children, and the Sources build phase — whereas an explicit file reference in xcproj looks roughly like this:
{
"kind": "group",
"path": "Sources",
"children": [
{ "path": "ContentView.swift", "target-membership": [ "MyApp/compile-sources" ] },
{ "path": "MyFramework.h", "target-membership": [ { "build-phase": "MyKit/headers", "header-role": "public" } ] }
]
}
A few details are worth noting. ContentView.swift has no kind because a file reference is the default. For an ordinary compiled file, membership can be a single string: MyApp/compile-sources is a compact reference combining the target name with the kind of build phase, and no UUID is involved. Only when extra attributes are needed does it expand into an object — marking a header as public no longer means editing some PBXBuildFile.settings.ATTRIBUTES.
What matters here isn’t that it’s “finally JSON,” but that these structural changes bring the textual form of the project file closer to the project semantics it describes.
Alongside this, Apple released the open-source xcode-project-format, providing models and tooling for the new format. Xcode 27.2 also ships xcprojformatter from that same project in /usr/bin. It validates and reformats project files that already use xcproj according to the spec — it is not a pbxproj-to-xcproj converter. For CI purposes, think of it as swift-format for project configuration files.
Impact on Other Approaches
As noted above, xcproj only changes how that build graph inside the bundle is expressed. It doesn’t alter the basic role of .xcodeproj, and it doesn’t displace project manifests like Tuist or XcodeGen. For existing workflows, little changes: generators keep generating projects, tools that modify projects keep modifying them, and each can adopt the new format at its own pace.
| Approach | Where truth lives | What it solves | After switching to xcproj |
|---|---|---|---|
Hand-maintained pbxproj | Xcode project | — (the default state) | Benefits directly; a good candidate for early migration |
xcconfig | Settings files | Reusing settings | Still useful, complementary to xcproj |
| Synchronized folder | Xcode project | Fewer diffs from adding/removing files | Still effective, and folds into the new project expression |
| XcodeGen | YAML | Generating projects from a manifest | Can adopt xcproj gradually; the manifest keeps its value |
| Tuist | Swift manifest | Generation + modularity + caching + rules | Workflow unchanged; awaiting further toolchain support |
| Bazel | BUILD | Hermetic builds / remote caching | The project-generation layer can adapt; build logic stands |
| SwiftPM | Package.swift | Describing a Swift Package with a package model | Largely unaffected |
| Project-modifying tools | Still the Xcode project | Changing dependencies or config in an existing project | Support varies; verify each tool before switching |
Independent developers and small teams still maintaining .xcodeproj directly stand to gain the most. Teams already using other generation or build tooling can wait for those tools to catch up; their underlying workflow won’t fundamentally change.
Progress, But Not a Revolution
Developers have complained about pbxproj for years. One answer the community arrived at was generators, turning the Xcode project itself into a build product. That works well, but in a sense it also shifts a responsibility that belongs to the IDE onto the authors of those tools.
Xcode 27.2’s decision to rewrite the project format at this particular moment is clearly about more than Git. Apple filed the article introducing the new format under Coding Intelligence. What the new format improves happens to be exactly the pain point coding agents hit hardest when operating on Xcode projects: no longer having to second-guess a thirty-year-old serialization convention just to change one piece of configuration.
For developers still maintaining .xcodeproj by hand, this is welcome progress — but not a revolution.
What the community has actually been waiting years for is a project manifest along the lines of Package.swift: something written by people, read by people, and reviewed by people, rather than a more modern encoding of the IDE’s internal database.
When swift package generate-xcodeproj was deprecated, people raised nearly the same request: project configuration should be easily readable and code reviewable. Apple’s answer then pointed elsewhere — Xcode-specific information shouldn’t find its way into a manifest meant for cross-platform Swift packages.
Apple has, in fact, shown another possibility. Swift Playgrounds uses .swiftpm together with AppleProductTypes.iOSApplication, already capable of describing an app you can develop on iPad and ship to the App Store. Yet the configuration carries a note at the top saying it is auto-generated and shouldn’t be edited by hand. At least so far, Apple still seems to prefer having tools maintain the project description rather than turning it into a public interface developers write themselves.
So xcproj feels less like switching tracks than like repaving the old track so agents can run on it.
Folders made the file system the source of truth for code again; JSON makes the remaining build graph more readable. After those two steps, imagining a project manifest genuinely aimed at developers — one you can write and review by hand — no longer feels as distant as it once did.
Whether Apple is willing to keep walking in that direction, we can only wait and see.