XCode 仮想フォルダの長年の問題と私のオープンソースツールによる解決策
Apple 開発者の職業病:Xcode 初期の仮想ディレクトリ使用により、ディレクトリ構造が混乱し、XcodeGen や Tuist などの最新ツールとの統合が困難に。

Photo by Saad Salim
この投稿の英語版:
Xcodeの長年の仮想ディレクトリ問題と私のオープンソースツールによる解決策
背景
チームの規模やプロジェクトの成長に伴い、Xcode プロジェクトファイル(.xcodeproj)のサイズは徐々に大きくなります。プロジェクトの複雑さによっては十万行、さらには百万行に達することもあります。複数人が複数のブランチで並行開発を行うため、コンフリクトが発生するのは避けられません。XcodeProj ファイルのコンフリクトは Storyboard や .xib のコンフリクトと同様に解決が難しいです。これらも純粋な記述ファイルであるため、コンフリクト解決時に他人が追加したファイルを誤って削除したり、他人が削除したファイルの参照が戻ってきてしまうことがよくあります。
もう一つの問題は、モジュール化が進むにつれて、Xcode プロジェクトファイル(.xcodeproj)でのモジュールの作成や管理のプロセスが非常に使いにくいことです。モジュールに変更があっても、Xcode プロジェクトファイルの Diff でしか確認できず、チームがモジュール化に向かうのを妨げています。
もし衝突防止だけが目的であれば、pre-commit で簡単に File Sorting を実行する方法があります。既存のスクリプトはGithubに多数あり、そのまま参考に設定可能です。
XCFolder
要点を簡単に 、Xcodeの初期の仮想フォルダをXcode内のディレクトリ構造に沿った実際のフォルダに変換するツールを開発しました。

スクロールして続きを見る…
モダンな Xcode プロジェクトファイル管理
具体的な考え方は、複数人で開発する際に Storyboard や .xib の使用を推奨しないのと同じで、「Xcode プロジェクトファイル」を管理するために、メンテナンスしやすく、反復可能で、コードレビューができるインターフェースが必要です。現在、市場で主に使われている無料ツールは以下の2つです:
-
XCodeGen :老舗のツールで、YAMLでXcodeプロジェクトの内容を定義し、それをXcodeプロジェクトファイル(.xcodeproj)に変換します。
YAMLで直接構造を定義できるため導入や学習コストが低いですが、モジュール化や依存管理機能は弱く、YAMLの動的設定も苦手です。 -
Tuist :近年登場した新しいツールで、Swift DSL を使って Xcode プロジェクトの内容を定義し、それを Xcode プロジェクトファイル (.xcodeproj) に変換します。
より安定かつ柔軟で、モジュール化や依存関係管理機能が内蔵されていますが、学習と導入のハードルはやや高めです。
どのツールを使っても、私たちのコアワークフローは以下のようになります:
-
Xcode プロジェクト設定ファイルの作成(XcodeGen の
project.yamlまたは Tuist のProject.swift) -
XcodeGen または Tuist を開発者および CI/CD サーバー環境に導入する
-
XcodeGen または Tuist を使って設定ファイルから
.xcodeprojXcode プロジェクトファイルを生成する -
/*.xcodeprojディレクトリファイルを.gitignoreに追加する -
開発者のワークフローを調整し、ブランチを切り替える際に設定ファイルから
.xcodeprojXcode プロジェクトファイルを生成するために XcodeGen または Tuist を実行する必要がある -
CI/CD のプロセスを調整する際、設定ファイルから
.xcodeprojXcode プロジェクトファイルを生成するために XcodeGen または Tuist を実行する必要がある -
完了
.xcodeproj の Xcode プロジェクトファイルは、XcodeGen または Tuist が YAML や Swift DSL の設定ファイルを元に生成します。同じ設定ファイルと同じツールのバージョンであれば、同じ結果が得られます。そのため、.xcodeproj ファイルを Git にコミットする必要はなくなり、将来的に .xcodeproj ファイルの競合が起こることを防げます。プロジェクト構成の変更やモジュールの追加・変更は、設定ファイルに戻って調整します。Yaml や Swift で書かれているので、簡単にイテレーションやコードレビューが可能です。
Tuist Swift サンプル: Project.swift
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 の例: project.yaml
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
ファイルディレクトリ構造
XCodeGen や Tuist はファイルの実際のディレクトリや位置に基づいて XCode プロジェクトファイル(.xcodeproj)のディレクトリ構造を生成します。実際のディレクトリが XCode プロジェクトのファイルディレクトリとなります。

したがって、ファイルの実際のディレクトリ位置が非常に重要であり、それを直接 Xcode プロジェクトのファイルディレクトリとして使用します。
これは現代の Xcode / Xcode プロジェクトにおいて、これら二つのディレクトリの位置が同じであることがごく普通のことですが、この問題こそ本記事で探究したいテーマです。
初期の Xcode における仮想ディレクトリの使用
初期の Xcode では、ファイルディレクトリ上で右クリックして「New Group」を選んでも実際のディレクトリは作成されません。ファイルはプロジェクトのルートディレクトリに置かれ、.xcodeproj の Xcode プロジェクトファイル内でファイル参照が行われます。そのため、ディレクトリは Xcode プロジェクトファイル内でのみ見え、実際には存在しません。

時代の変化とともに、AppleはXcodeでこの奇妙な設計を徐々に廃止しました。後のXcodeでは「New Group with Folder」が追加され、デフォルトで実体フォルダを作成し、作成したくない場合は「New Group without Folder」を選択するようになりました。そして現在(Xcode 16)では「New Group」のみが残り、実体フォルダに基づいて自動的にXcodeプロジェクトのファイル構成が生成されます。
仮想フォルダの問題
-
XcodeGen や Tuist が使えない理由は、どちらも実際のディレクトリ構造が必要で、Xcode プロジェクトファイル (.xcodeproj) を生成するためです。
-
Code Review の難しさ:Git の Web GUI ではディレクトリ構造が表示されず、ファイルがすべて平坦に並んで見えます。
-
DevOps やサードパーティツールの統合が困難:例えば Sentry や Github はディレクトリごとに警告や自動アサインを設定できますが、ディレクトリがなくファイルだけだと設定できません。
-
プロジェクトのディレクトリ構造が非常に複雑で、多くのファイルがルートディレクトリに平坦化されています。
古いプロジェクトで、初期に仮想フォルダの問題に気づかなかった場合、仮想フォルダ内に3,000以上のファイルがあることもあり、手動で移動すると終わる頃には辞職して売り子になっているかもしれません。これはまさに「Apple開発者の職業病 😔」と言えるでしょう。
Xcode プロジェクトの仮想ディレクトリから実ディレクトリへの変換
上述の理由から、Xcodeプロジェクトの仮想フォルダを実際のフォルダに変換することが急務です。そうしなければ、今後のプロジェクトのモダナイズや効率的な開発プロセスの推進ができません。
❌ Xcode 16 の「フォルダに変換」オプション

XCode 16
昨年 Xcode 16 がリリースされたとき、このメニューの新しいオプションに注目していました。本来の期待は、仮想ディレクトリのファイルを自動で実際のディレクトリに変換してくれることでした。
しかし実際にはそうではなく、まずファイルをディレクトリに配置し、対応する実際の場所に置く必要があります。「Convert to folder」をクリックすると、新しいXcodeプロジェクトのディレクトリ設定方式「PBXFileSystemSynchronizedRootGroup」に変更されます。正直言って、変換自体には全く役に立たず、これは変換後に新しいディレクトリ設定方式にアップグレードできるというだけです。
目次が作成されておらず、ファイルが正しく配置されていない場合、「Convert to folder」をクリックすると以下のエラーが表示されます:
関連フォルダがありません
各グループには関連付けられたフォルダが必要です。以下のグループはフォルダと関連付けられていません:
• xxxx
ファイルインスペクタを使って各グループにフォルダを関連付けるか、内容を別のグループに移動した後にグループを削除してください。
🫥 オープンソースプロジェクト venmo / synx
Githubで長時間検索した結果、Rubyで書かれたこの仮想ディレクトリを実ディレクトリに変換するオープンソースツールしか見つかりませんでした。実際に動かしてみると効果はありましたが、約10年更新されておらず、多くのファイルは手動で対応・移動する必要があり、完全な変換はできなかったため断念しました。
しかし、このオープンソースプロジェクトに触発されて、自分で変換ツールを開発しようと思いました。
✅ 私のオープンソースプロジェクト ZhgChgLi / XCFolder
純粋な Swift で開発されたコマンドラインツールで、XcodeProj を使って .xcodeproj Xcode プロジェクトファイルを解析し、すべてのファイルの所属ディレクトリを取得し、ディレクトリを解析して仮想フォルダを実際のフォルダに変換し、ファイルを正しい場所に移動し、最後に .xcodeproj のディレクトリ設定を調整することで変換を完了します。

使用方法
git clone https://github.com/ZhgChgLi/XCFolder.git
cd ./XCFolder
swift run XCFolder YOUR_XCODEPROJ_FILE.xcodeproj ./Configuration.yaml
例えば:
swift run XCFolder ./TestProject/DCDeviceTest.xcodeproj ./Configuration.yaml
CI/CD モード( Non Interactive Mode ):
swift run XCFolder YOUR_XCODEPROJ_FILE.xcodeproj ./Configuration.yaml --is-non-interactive-mode
Configuration.yaml では実行したいパラメータを設定できます:
# 無視するディレクトリ、解析や変換は行わない
ignorePaths:
- "Pods"
- "Frameworks"
- "Products"
# 無視するファイルタイプ、変換や移動はされない
ignoreFileTypes:
- "wrapper.framework" # フレームワーク
- "wrapper.pb-project" # Xcode プロジェクトファイル
#- "wrapper.application" # アプリケーション
#- "wrapper.cfbundle" # バンドル
#- "wrapper.plug-in" # プラグイン
#- "wrapper.xpc-service" # XPC サービス
#- "wrapper.xctest" # XCTest バンドル
#- "wrapper.app-extension" # アプリ拡張
# ディレクトリを作成しファイルを移動するだけで、.xcodeproj のディレクトリ設定は変更しない
moveFileOnly: false
# git mv コマンドを優先してファイルを移動する
gitMove: true
⚠️ 実行前の注意:
-
Git に未コミットの変更がないことを必ず確認してください。スクリプトが誤ってプロジェクトディレクトリを汚染するのを防ぐためです。
(スクリプト実行時にチェックを行い、未コミットの変更がある場合は❌ Error: There are uncommitted changes in the repositoryエラーを投げます) -
デフォルトでは、
git mvコマンドを優先してファイルを移動し、git のファイル履歴を完全に保持します。移動に失敗した場合や Git プロジェクトでない場合のみ、FileSystem Move を使用してファイルを移動します。
実行完了を待つだけでOK:


⚠️ 実行後の注意:
-
プロジェクトディレクトリに漏れている(赤色の)ファイルがないか確認してください。数が少なければ手動で修正可能ですが、多い場合は Configuration.yaml の ignorePaths、ignoreFileTypes の設定が正しいか確認してください。または Issueを作成 して教えてください。
-
Build Setting の関連パス(例:
LIBRARY_SEARCH_PATHS)を手動で変更する必要があるか確認してください。 -
クリーン&ビルドを試してみてください。
-
もし現在の
.xcodeprojXcode プロジェクトファイルを管理するのが面倒なら、XcodeGen や Tuist を使ってディレクトリやファイルを直接再生成することもできます
スクリプトの修正:
直接クリックで ./Package.swift を開き、スクリプト内容を調整できます。
その他の開発メモ
-
得意先の XcodeProj を利用し、Swift オブジェクトで
.xcodeprojXcode プロジェクトファイルの内容 に簡単にアクセスできます。 -
同様に Clean Architecture を採用して開発しています。
-
PBXGroup の設定で path がなく name のみの場合は仮想ディレクトリ、逆に path がある場合は実体ディレクトリとなります。
-
Xcode 16 の新しいディレクトリ設定
PBXFileSystemSynchronizedRootGroupは、ルートディレクトリを宣言するだけで実体ディレクトリから自動的に解析され、各ディレクトリやファイルを.xcodeprojのプロジェクトファイル内に個別に宣言する必要がありません。 -
直接 SPM(Package.swift)方式でコマンドラインツールを開発するのは本当に便利です!
Post MediumからZMediumToMarkdownによって変換。



コメント