Storyboard 在開發機上可以正常開啟,合併後卻可能讓遠端封存工作執行到一半才失敗。這時重新下載相依套件、啟動模擬器或清除 DerivedData 都無濟於事,因為真正的問題可能只是失效的 outlet、錯誤的自訂類別模組,或某個 XIB 使用了目前部署目標不支援的屬性。對雲端 Mac 上的持續整合而言,更有效率的做法是先以 ibtool 個別編譯介面資源,再決定是否進入完整建置。
將介面資源檢查提前到建置前
ibtool 隨 Xcode 提供,應透過 xcrun 呼叫,而不是在腳本中寫死工具路徑。它能讀取 Storyboard 與 XIB,輸出 errors、warnings 和 notices,並將來源檔案編譯成 storyboardc 或 nib。這個階段不需要啟動模擬器,適合安排在解析相依套件之後、執行 xcodebuild archive 之前。
先確認執行節點實際使用的開發者目錄:
set -euo pipefail
xcode-select -p
xcrun --find ibtool
xcodebuild -version
xcrun ibtool --version
這些資訊應寫入工作日誌。若團隊在同一台雲端 Mac 上保留多個 Xcode 版本,管線必須明確設定 DEVELOPER_DIR,並在工作結束後清除這個環境變數,以免後續工作繼承錯誤的工具鏈。
介面檔案能由圖形化編輯器開啟,不代表它能在指定的 Xcode、部署目標與模組環境中成功編譯。CI 檢查的目標是重現發佈工具鏈,而不是重現某位開發者的桌面環境。
掃描並個別編譯 Storyboard 與 XIB
以下腳本會掃描儲存庫中的介面檔案,並為每個輸入建立獨立的輸出目錄。排除建置目錄與相依套件目錄十分重要,否則可能重複檢查產生的檔案或第三方資源。
#!/usr/bin/env bash
set -euo pipefail
root="${1:-$PWD}"
target="${IPHONEOS_DEPLOYMENT_TARGET:-16.0}"
work="${TMPDIR:-/tmp}/ibtool-preflight"
log="$work/ibtool.log"
rm -rf "$work"
mkdir -p "$work/out"
: > "$log"
find "$root" \
\( -path "*/DerivedData/*" -o -path "*/build/*" -o -path "*/Pods/*" \) -prune \
-o \( -name "*.storyboard" -o -name "*.xib" \) -print0 |
while IFS= read -r -d '' source; do
relative="${source#"$root"/}"
safe_name="$(printf '%s' "$relative" | tr '/ ' '__')"
case "$source" in
*.storyboard)
output="$work/out/${safe_name%.storyboard}.storyboardc"
;;
*.xib)
output="$work/out/${safe_name%.xib}.nib"
;;
esac
printf 'Checking %s
' "$relative" | tee -a "$log"
xcrun ibtool \
--errors \
--warnings \
--notices \
--minimum-deployment-target "$target" \
--compile "$output" "$source" 2>&1 | tee -a "$log"
done
腳本使用暫存目錄,不會汙染儲存庫。失敗時應保留 ibtool.log;成功時可以刪除編譯結果,只上傳日誌摘要。不要將暫存輸出提交至版本庫,也不要讓多個平行工作共用固定目錄。
釐清獨立預檢與專案建置的界線
獨立呼叫 ibtool 可以找出 XML 損壞、部分連線異常、屬性不相容與編譯錯誤,但它並不了解完整 target 的所有建置設定。自訂檢視控制器屬於哪個模組、某項資源是否加入 Copy Bundle Resources,以及條件式編譯後的類別是否存在,仍須交由 xcodebuild 驗證。
建議將檢查分成兩層:
| 層級 | 輸入 | 主要發現 | 失敗處理 |
|---|---|---|---|
| 快速預檢 | Storyboard、XIB | 檔案損壞、屬性不相容、基本連線錯誤 | 立即停止 |
| 專案建置 | workspace、project、Scheme | 模組解析、資源歸屬、連結與簽署環境 | 保留完整建置日誌 |
若獨立預檢通過但專案建置失敗,應優先檢查 Target Membership、Module 欄位、類別名稱重構紀錄,以及部署目標的繼承關係。反過來,若預檢已經失敗,就沒有必要繼續執行更耗時的封存。
將本地化檔案納入同一門檻
Storyboard 本地化常見兩種形式:為每種語言維護獨立資源,或使用 Base Internationalization 搭配 .strings。前者容易發生物件 ID 漂移,後者則容易留下已刪除的鍵。先用 plutil 檢查 .strings 的基本格式:
find . -name "*.strings" -print0 |
while IFS= read -r -d '' file; do
plutil -lint "$file"
done
對於 Base Storyboard,可以產生一份目前的鍵集合,再與儲存庫中的本地化檔案比較:
mkdir -p "${TMPDIR:-/tmp}/ibtool-strings"
xcrun ibtool \
--generate-strings-file "${TMPDIR:-/tmp}/ibtool-strings/Main.strings" \
"App/Base.lproj/Main.storyboard"
plutil -lint "${TMPDIR:-/tmp}/ibtool-strings/Main.strings"
產生的檔案只用於比較,不應直接覆寫譯文。較穩妥的流程是擷取鍵名、排序後進行差異檢查:新增的鍵加入待翻譯清單,刪除的鍵產生清理提示,既有鍵的文字仍由本地化流程維護。這樣既能發現缺漏,也不會誤將現有翻譯替換成 Base 文字。
管理警告基準與常見誤判
將所有輸出一律視為失敗,短期看似嚴格,實際上會讓團隊逐漸習慣忽略紅燈。更具可執行性的規則是:errors 立即判定失敗;warnings 與儲存庫中已審查的基準比較,只阻擋新增項目;notices 則保留在建置產出中並定期清理。
常見的疑難排解順序如下:
- 確認
DEVELOPER_DIR與完整建置一致。 - 核對
IPHONEOS_DEPLOYMENT_TARGET是否來自專案的實際設定。 - 檢查檔案路徑的大小寫,尤其是重新命名後只變更字母大小寫的資源。
- 搜尋失效的 outlet、action 與自訂類別名稱。
- 確認暫存目錄依工作隔離,避免平行編譯相互覆寫輸出。
- 使用相同的 Scheme 再執行一次不簽署的建置,以驗證模組環境。
警告基準應保存穩定的識別資訊,避免記錄暫時性的絕對路徑。升級 Xcode 時,先在獨立分支執行預檢,審查新增的診斷訊息,再更新基準;不要在升級提交中直接篩除所有新警告。
建立可回復的導入順序
首次導入時,可以只收集一週的日誌,不阻擋合併。確認誤報來源後,再將具確定性的 errors 設為失敗,接著逐步啟用新增 warning 的門檻。腳本本身應固定存放於儲存庫中,供本機與雲端 Mac 共用,避免 CI 設定中藏有另一套實作。
最終檢查項目包括:工具鏈已固定;掃描目錄有明確的排除項目;每個工作使用獨立的暫存目錄;部署目標來自專案設定;日誌在失敗時作為附件保留;本地化鍵只比較而不覆寫;完整的 Scheme 建置仍會執行。如此一來,介面資源問題便能在數十秒的預檢階段顯現,而不是等到封存尾聲才留下難以重現的錯誤。
常見問題
ibtool 預檢可以取代完整的 Xcode 建置嗎?
不可以。它適合快速檢查介面資源結構與編譯相容性,最後仍須使用專案的實際 Scheme 完成建置與測試。
為什麼本機能開啟的 Storyboard 在 CI 仍會失敗?
常見原因是 CI 選到不同的 Xcode、部署目標不一致、路徑大小寫錯誤,或自訂類別所在模組未進入目前的建置環境。
是否應該把 ibtool 的 notices 全部視為失敗?
不建議。先以 errors 阻擋流程,讓 warnings 與既有基準比較,並將 notices 保存為診斷紀錄。
在專屬實體節點上執行下一項任務
選擇節點與計費週期,使用固定的 M4、16GB RAM 和 256GB SSD 設定開始部署。實際可用性以控制台即時回傳為準。