Post

Xcode Virtual Directory Issues: Streamline Your Project Structure with Open Source Tools

Developers facing Xcode virtual directory chaos can simplify integration with modern tools like XcodeGen and Tuist using a proven open source solution that restores project clarity and boosts workflow efficiency.

Xcode Virtual Directory Issues: Streamline Your Project Structure with Open Source Tools

点击这里查看本文章简体中文版本。

點擊這裡查看本文章正體中文版本。

This post was translated with AI assistance — let me know if anything sounds off!


Investigation of the Eternal XCode Virtual Directory Issue and My Open Source Tool Solution

Apple developer occupational hazard: Early use of virtual directories in Xcode caused messy directory structures and made integration with modern tools like XcodeGen and Tuist difficult.

Photo by [Saad Salim](https://unsplash.com/@saadx?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash){:target="_blank"}

Photo by Saad Salim

English version of this post:

Exploring the Long-Standing Issues of XCode Virtual Directories and My Open Source Tool Solution

Background

As the team size and project grow, the XCode project file (.xcodeproj) increases in size. Depending on the project’s complexity, it can reach hundreds of thousands or even millions of lines. With multiple people working on different branches simultaneously, conflicts are inevitable; XcodeProj file conflicts are as hard to resolve as Storyboard/.xib conflicts. Since it is also a pure descriptor file, it’s easy to accidentally remove files others added or bring back references to files others deleted during conflict resolution.

Another issue is that as modularization progresses, the process of creating and managing modules in the XCode project file (.xcodeproj) is very unfriendly. Changes to modules can only be viewed through diffs in the XCode project file, which hinders the team’s move toward modularization.

If it’s just to prevent conflicts, you can simply do File Sorting in pre-commit. There are many existing scripts on Github you can directly refer to for setup.

XCFolder

In short , developed a tool that converts XCode’s early virtual directories into physical directories based on the directory structure in XCode.

Scroll down to continue reading the story…

Modernizing Xcode Project File Management

Just like we discourage using Storyboard or .xib in team development, we need a well-maintained, iterative, and code-reviewable interface to manage the “XCode project files.” Currently, there are two popular free tools available on the market:

  • XCodeGen: A classic tool that uses YAML to define Xcode project content and then converts it into an Xcode project file (.xcodeproj).
    It directly uses YAML to define the structure, making it easier to learn and use. However, it has weaker support for modularization, dependency management, and dynamic YAML configuration.

  • Tuist: A newer tool released in recent years that uses Swift DSL to define Xcode project content, then converts it into an Xcode project file (.xcodeproj).
    It is more stable and flexible, with built-in modularization and dependency management features, but has a higher learning curve and adoption barrier.

No matter which one, our core workflow will become:

  1. Create XCode Project Configuration File (XCodeGen project.yaml or Tuist Project.swift)

  2. Adding XCodeGen or Tuist to Developer and CI/CD Server Environments

  3. Generate .xcodeproj XCode Project Files Using XCodeGen or Tuist via Configuration Files

  4. Add /*.xcodeproj directory files to .gitignore

  5. Adjust the developer workflow: when switching branches, run XCodeGen or Tuist to generate the .xcodeproj Xcode project file through configuration files.

  6. Adjust the CI/CD process to run XCodeGen or Tuist to generate the .xcodeproj Xcode project file through configuration files.

  7. Completed

.xcodeproj XCode project files are generated by XCodeGen or Tuist based on YAML or Swift DSL configuration files. The same configuration file and tool version will produce the same result; therefore, we don’t need to commit .xcodeproj files to Git. This ensures no future .xcodeproj file conflicts. Changes in project structure or module additions are made by updating the configuration files. Since these are written in YAML or Swift, we can easily iterate and perform code reviews.

Tuist Swift Example: Project.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import ProjectDescription

let project = Project(
    name: "MyApp",
    targets: [
        Target(
            name: "MyApp",
            platform: .iOS,
            product: .app,
            bundleId: "com.example.myapp",
            deploymentTarget: .iOS(targetVersion: "15.0", devices: [.iphone, .ipad]),
            infoPlist: .default,
            sources: ["Sources/**"],
            resources: ["Resources/**"],
            dependencies: []
        ),
        Target(
            name: "MyAppTests",
            platform: .iOS,
            product: .unitTests,
            bundleId: "com.example.myapp.tests",
            deploymentTarget: .iOS(targetVersion: "15.0", devices: [.iphone, .ipad]),
            infoPlist: .default,
            sources: ["Tests/**"],
            dependencies: [.target(name: "MyApp")]
        )
    ]
)

XCodeGen YAML Example: project.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
name: MyApp
options:
  bundleIdPrefix: com.example
  deploymentTarget:
    iOS: '15.0'

targets:
  MyApp:
    type: application
    platform: iOS
    sources: [Sources]
    resources: [Resources]
    info:
      path: Info.plist
      properties:
        UILaunchScreen: {}
    dependencies:
      - framework: Vendor/SomeFramework.framework
      - sdk: UIKit.framework
      - package: Alamofire

  MyAppTests:
    type: bundle.unit-test
    platform: iOS
    sources: [Tests]
    dependencies:
      - target: MyApp

File Directory Structure

XCodeGen or Tuist generates the XCode project file (.xcodeproj) directory structure based on the actual file directories and locations. The actual directory is the XCode project file directory.

Therefore, the actual directory location of the file is very important, as we will directly use it as the XCode project file directory.

In modern XCode / XCode projects, it is quite common for these two directories to be located at the same level, but this topic is what this article aims to explore.

Early Xcode Used Virtual Directories

In early versions of XCode, right-clicking the file directory and selecting “New Group” did not create a physical folder. Files were placed in the project’s root directory and referenced in the .xcodeproj XCode project file, so the folder existed only within the XCode project file and not physically.

As time has passed, Apple has gradually phased out this odd design in XCode. Later versions of XCode introduced “New Group with Folder” by default, and to avoid creating a physical folder, you had to select “New Group without Folder.” Now (XCode 16), there is only “New Group,” which automatically generates the XCode project file directory based on the physical folder structure.

Problems with Virtual Directories

  • Cannot use XCodeGen or Tuist because both require a physical directory location to generate the XCode project file (.xcodeproj).

  • Code Review Difficulty: The directory structure is not visible on the Git Web GUI; all files appear in a flat list.

  • DevOps, Difficulty Integrating Third-Party Tools: For example, Sentry and GitHub can assign alerts or Auto-Assigner based on directories, but settings cannot be configured if there are only files without directories.

  • The project directory structure is extremely complex, with many files flattened in the root directory.

For an old project that didn’t handle the virtual directory issue early, there are over 3,000 virtual directory files. Manually matching and moving them would be so exhausting you’d probably quit to sell rice cakes. This can truly be called “Apple Developer Occupational Hazard 😔.”

Convert Xcode Project Virtual Folder to Physical Folder

For all these reasons, we urgently need to convert the virtual directories in the XCode project into physical directories; otherwise, future project modernization and more efficient development processes cannot proceed.

❌ Xcode 16 “Convert to folder” Option

XCode 16

XCode 16

Last year, when XCode 16 was released, I noticed this new menu option. My original expectation was that it could automatically convert virtual directory files into physical directories.

But in reality, it requires you to first create the folder structure and place the files in the correct physical locations. After clicking “Convert to folder,” it will change to the new XCode project directory setting called PBXFileSystemSynchronizedRootGroup. To be honest, this has no effect on the conversion itself; it is more about upgrading to the new directory setting after the conversion.

The table of contents is not created, and the files are not properly placed. Clicking “Convert to folder” will cause the following error:

1
2
3
4
Missing Associated Folder
Each group must have an associated folder. The following groups are not associated with a folder:
• xxxx
Use the file inspector to associate a folder with each group, or delete the groups after moving their content to another group.

🫥 Open Source Projects venmo / synx

After searching on Github for a long time, I only found this open-source tool written in Ruby for virtual to physical directory conversion. It works in practice, but since it hasn’t been updated for about 10 years, many files still require manual matching and moving. It can’t fully convert, so I gave up.

However, I am still grateful for the inspiration from this open-source project, which made me consider developing my own conversion tool.

✅ My open-source projects ZhgChgLi / XCFolder

Command Line Tools developed purely in Swift, based on XcodeProj, parse .xcodeproj Xcode project files, read all files to get their directories, convert virtual directories into physical ones by parsing the directories, move files to the correct locations, and finally adjust the .xcodeproj Xcode project directory settings to complete the conversion.

Usage

1
2
3
git clone https://github.com/ZhgChgLi/XCFolder.git
cd ./XCFolder
swift run XCFolder YOUR_XCODEPROJ_FILE.xcodeproj ./Configuration.yaml

For Example:

1
swift run XCFolder ./TestProject/DCDeviceTest.xcodeproj ./Configuration.yaml

CI/CD Mode ( Non Interactive Mode ):

1
swift run XCFolder YOUR_XCODEPROJ_FILE.xcodeproj ./Configuration.yaml --is-non-interactive-mode

Configuration.yaml can be used to set the desired execution parameters:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Directories to ignore, will not be parsed or converted
ignorePaths:
- "Pods"
- "Frameworks"
- "Products"

# File types to ignore, will not be converted or moved
ignoreFileTypes:
- "wrapper.framework" # Frameworks
- "wrapper.pb-project" # Xcode project files
#- "wrapper.application" # Applications
#- "wrapper.cfbundle" # Bundles
#- "wrapper.plug-in" # Plug-ins
#- "wrapper.xpc-service" # XPC services
#- "wrapper.xctest" # XCTest bundles
#- "wrapper.app-extension" # App extensions

# Only create directories and move files, do not adjust .xcodeproj Xcode project directory settings
moveFileOnly: false

# Prefer using git mv command to move files
gitMove: true

⚠️ Please Note Before Execution:

  • Please ensure there are no uncommitted changes in Git to avoid the script accidentally corrupting your project directory.
    (The script will check this and throw an error ❌ Error: There are uncommitted changes in the repository if any uncommitted changes are found.)

  • By default, prioritize using the git mv command to move files to ensure complete git file log records. If the move fails or the project is not a Git repository, then use FileSystem Move to move the files.

Just wait for the execution to complete:

⚠️ Please note after execution:

  • Please check if there are any missing (in red) files in the project directory. If the number is small, you can fix them manually. If there are many, please verify whether the ignorePaths and ignoreFileTypes settings in Configuration.yaml are correct, or create an Issue to let me know.

  • Check if related paths in Build Settings, e.g., LIBRARY_SEARCH_PATHS, need to be manually adjusted.

  • Suggest trying Clean & Build to see if it helps.

  • If you don’t want to manage the current .xcodeproj XCode project file, you can directly start using XCodeGen or Tuist to regenerate the directory files.

Modify Script:

Simply click ./Package.swift to open the project and modify the script content.

Other Development Notes

  • Thanks to XcodeProj, we can easily access the contents of .xcodeproj Xcode project files using Swift objects.

  • Also developed using the Clean Architecture framework.

  • In PBXGroup settings, if there is no path but only a name, it is a virtual directory; otherwise, it is a physical directory.

  • XCode 16’s new directory setting PBXFileSystemSynchronizedRootGroup only requires declaring the root directory, which will be automatically resolved from the physical directory. There is no need to declare each directory and file inside the .xcodeproj Xcode project file.

  • Using SPM (Package.swift) to develop Command Line Tools is really convenient!

If you have any questions or feedback, feel free to contact me.


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.