Post

XCode 虛擬目錄萬年問題探究與我的開源工具解決方案

Apple 開發者職業傷害之 Xcode 早期使用虛擬目錄,導致目錄結構混亂且難以整合 XcodeGen, Tuist 等現代工具。

XCode 虛擬目錄萬年問題探究與我的開源工具解決方案

ℹ️ℹ️ℹ️ Click here to view the English version of this article, translated by OpenAI.


XCode 虛擬目錄萬年問題探究與我的開源工具解決方案

Apple 開發者職業傷害之 Xcode 早期使用虛擬目錄,導致目錄結構混亂且難以整合 XcodeGen, Tuist 等現代工具。

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

背景

隨著團隊規模與專案的成長,XCode 專案檔 ( .xcodeproj) 的體積會逐漸增大,依照專案複雜程度可以到十萬行、甚至百萬行,加上多人多條線並行開發,免不了會出現衝突問題; XcodeProj 檔案一旦出現衝突就跟 Storyboard / .xib 的衝突一樣難解 ,因也是純描述檔,很容易在解衝突時不小心把別人新增的檔案移除或是別人移除的檔案參照又跑回來。

另外的問題是隨著模組化推進,在 XCode 專案檔 ( .xcodeproj) 建立、管理模組的流程非常不友善,模組有變動一樣只能從 XCode 專案檔 的 Diff 中查看,整體不利於團隊朝模組化邁進。

如果只是為了防止衝突可以簡單地在 pre-commit 做 File Sorting ,現有的腳本 Github 上很多,可以直接參考設置。

XCFolder

長話短說 ,開發了一個工具可以幫你把 XCode 早期的虛擬目錄轉依照 XCode 裡的目錄結構換成實體目錄。

下滑繼續看故事…

現代化 Xcode Project 檔案管理

具體思維同我們在多人開發時不鼓勵使用 Storyboard or .xib 一樣, 我們需要一個好維護、可迭代、可被 Code Review 的介面來管理「XCode 專案檔」 ,目前市場主流有兩套免費工具可以使用:

  • XCodeGen :老牌工具,使用 YAML 定義 XCode 專案內容,再透過它轉換成 XCode 專案檔 ( .xcodeproj)。 直接使用 YAML 定義結構、導入難度及學習成本較低、但是對模組化或是依賴管理功能較弱、YAML 動態設置弱。
  • Tuist :近幾年出的新工具,使用 Swift DSL 定義 XCode 專案內容,再透過它轉換成 XCode 專案檔 ( .xcodeproj)。 更穩定多變,內建模組化及依賴管理功能,但學習與導入門檻較高。

不論是那一套我們的核心工作流程都會變成是:

  1. 建立 XCode 專案內容設定檔 (XCodeGen project.yaml or Tuist Project.swift )
  2. 將 XCodeGen or Tuist 加入開發者與 CI/CD Server 環境中
  3. 使用 XCodeGen or Tuist 透過設定檔產生 .xcodeproj XCode 專案檔
  4. /*.xcodeproj 目錄檔案 加入 .gitignore
  5. 調整開發者流程,切換 Branch 時需要跑 XCodeGen or Tuist 透過設定檔產生 .xcodeproj XCode 專案檔
  6. 調整 CI/CD 流程,需要跑 XCodeGen or Tuist 透過設定檔產生 .xcodeproj XCode 專案檔
  7. 完成

.xcodeproj XCode 專案檔是由 XCodeGen or Tuist 基於 YAML of Swift DSL 設定檔產生的,同一份設定檔 & 同個工具版本會產生相同結果;因此我們不需要再把 .xcodeproj XCode 專案檔上到 Git 當中,就此可以保證以後不會再有 .xcodeproj 檔案衝突;專案構建的改變、模組的新增改動我們都會回到定義的設定檔中做調整,因為是 Yaml or Swift 寫成的,我們可以很輕易地進行迭代跟 Code Review。

Tuist Swift 範例: 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 範例: 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

檔案目錄結構

XCodeGen or Tuist 會依照檔案實際目錄、位置產生 XCode 專案檔 ( .xcodeproj) 的目錄結構, 實際目錄即為 XCode 專案檔案目錄

因此檔案的實際目錄位置就很重要,我們會直接用它來當 XCode 專案檔案目錄。

這在現代的 XCode / XCode 專案中,這兩個目錄的位置相等是再日常不過的事,但這個議題就是本文想要探究的項目。

早期 Xcode 使用虛擬目錄

在早期的 XCode 中在檔案目錄上按右鍵「New Group」是不會建立實際目錄的,檔案會被放在專案根目錄下並在 .xcodeproj XCode 專案檔做 File Reference,因此目錄只有在 XCode 專案檔中可見,實際沒有。

隨著時代演變,Apple 在 XCode 上逐漸汰換了這個詭異的設計,後來的 XCode 從加上「New Group with Folder」到預設會建實體目錄、不要建要選「New Group without Folder」在到 現在 (XCode 16) 只剩「New Group」+ 自動根據實體目錄產生XCode 專案檔案目錄

虛擬目錄的問題

  • 無法使用 XCodeGen or Tuist,因為都需要實體目錄位置產生 XCode 專案檔 ( .xcodeproj)。
  • Code Review 困難:在 Git Web GUI 上看不到目錄結構,整個檔案都是攤平的。
  • DevOps, 第三方工具整合困難:例如 Sentry, Github 可以依照目錄派發警告或 Auto-Assigner,沒有目錄只有檔案無法設定。
  • 專案目錄結構極為複雜,一大堆檔案攤平在根目錄。

對於一個年代久遠並且沒有在早期意識到虛擬目錄問題的專案,虛擬目錄的檔案有 3,000+ 個,手動對照搬移的話搬完應該就辭職去賣狀元糕了,這真的可以稱得上是「 Apple 開發者職業傷害 😔 」。

Xcode 專案虛擬目錄 轉 實體目錄

基於上述種種原因,我們急迫的需要把 XCode 專案的虛擬目錄轉成實體目錄,否則後續的專案現代化、更高效的開發流程都無法推進。

❌ Xcode 16 「Convert to folder」選項

XCode 16

XCode 16

去年 XCode 16 剛出時有關注到這個選單新選項,本來的期待是它可以自動幫我們把虛擬目錄檔案轉換成實體目錄。

但實際並不然,他需要你先把檔案建好目錄、放到對應的實體位置,點擊「Convert to folder」後會幫你改成新的 XCode Project 目錄設定方式「 PBXFileSystemSynchronizedRootGroup 」,說實話 對轉換沒有任何用,這比較是轉換後可以升級成新的目錄設定方式。

目錄沒有建立、檔案沒有放好,點「Convert to folder」就會報以下錯誤:

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.

🫥 開源專案 venmo / synx

在 Github 搜尋許久只找到這個使用 Ruby 撰寫的虛擬轉實體目錄開源專案工具,實際跑下來是有效果的但因年久失修 (~= 10 年未更新) 很多檔案還是需要手動對應搬移,無法完全轉換,因此放棄。

不過還是很感謝這個開源專案的啟發,我才想說可以自己開發轉換工具。

✅ 我的開源專案 ZhgChgLi / XCFolder

使用純 Swift 開發的 Command Line Tools,基於 XcodeProj 解析 .xcodeproj XCode 專案檔,讀取所有檔案取得所屬目錄、解析目錄將虛擬目錄轉換成實體目錄,將檔案搬移到正確位置、最後再調整 .xcodeproj XCode 專案檔目錄設定,即可完成轉換。

使用方式

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 模式 ( Non Interactive Mode ):

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

Configuration.yaml 可以設定想要的執行參數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 忽略的目錄,不會解析轉換
ignorePaths:
- "Pods"
- "Frameworks"
- "Products"

# 忽略的檔案類型,不會被轉換搬移
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

# 只建立目錄、搬移檔案,不調整 .xcodeproj XCode 專案檔目錄設定
moveFileOnly: false

# 優先使用 git mv 指令搬移檔案
gitMove: true

⚠️ 執行前請注意:

  • 請確保 Git 無任何未 Commit 改變,因為怕腳本有誤污染你的專案目錄 (腳本執行會檢查,若有未 Commit 改變會拋錯誤 ❌ Error: There are uncommitted changes in the repository )
  • 預設優先使用 git mv 指令搬移檔案以確保 git file log 紀錄完整,如果搬移失敗或是非 Git 專案才會使用 FileSystem Move 搬移檔案。

等待執行完成即可:

⚠️ 執行後請注意:

  • 請檢查專案目錄是否有遺漏(紅色)的檔案,數量少可以手動修正,數量多請確認 Configuration.yaml 裡的 ignorePaths, ignoreFileTypes 設定是否正確,或是 建立一個 Issue 讓我知道。
  • 檢查 Build Setting 裡的相關路徑 e.g. LIBRARY_SEARCH_PATHS 是否需要手動更動路徑
  • 建議 Clean & Build 看看
  • 如果懶得管現在的 .xcodeproj XCode 專案檔,也可以直接開始使用 XCodeGen or Tuist 直接重新產生目錄檔案

修改腳本:

直接點擊 ./Package.swift 就能開啟專案調整腳本內容。

其他開發隨記

  • 得力於 XcodeProj 我們可以很輕易的使用 Swift 物件方式存取 .xcodeproj XCode 專案檔案內容
  • 同樣使用 Clean Architecture 架構開發
  • PBXGroup 設定中若沒有 path 只有 name 則為虛擬目錄,反之則為實體目錄
  • XCode 16 新的目錄設定 PBXFileSystemSynchronizedRootGroup 只需宣告主目錄就會自動從實體目錄解析,不需要再把每個目錄跟檔案都宣告在 .xcodeproj XCode 專案檔案內
  • 直接用 SPM (Package.swift) 方式開發 Command line tool 真的很方便!

有任何問題及指教歡迎 與我聯絡


本文首次發表於 Medium ➡️ 前往查看

ZMediumToMarkdownMedium-to-jekyll-starter 提供自動轉換與同步技術。

Improve this page on Github.

Buy me a beer

594 Total Views
Last Statistics Date: 2025-04-09 | 341 Views on Medium.
This post is licensed under CC BY 4.0 by the author.