多人同時修改 Storyboard 或 XIB 後,XML 合併看似成功,真正的問題卻可能要到完整封存時才浮現:連線所指向的物件已遭刪除、介面檔案無法編譯、在地化 strings 格式損毀,或兩個模組的同名輸出彼此覆寫。與其等 ArmVMS 上的雲端 Mac 跑完整套建置後才報錯,不如先使用 Xcode 內建的 ibtool 進行獨立預檢。
先釐清 ibtool 能檢查哪些內容
ibtool 是 Interface Builder 資源的命令列編譯器。它能獨立讀取 .storyboard 與 .xib,回報 XML 結構、物件連線、部分屬性設定,以及編譯過程中的警告。這類檢查通常不必先編譯業務程式碼,很適合安排在提交檢查或 CI 流程的前段。
它無法取代完整建置。自訂類別是否確實存在、Swift 型別是否相符、資源是否正確加入目標,以及最終連結能否完成,仍須交由 xcodebuild 驗證。合理的執行順序是先跑 ibtool,通過後再建置並測試目標工程。
應將 ibtool 視為介面資源的語法與結構檢查器,而不是最終交付的驗收工具。它的價值在於快速失敗,而非涵蓋所有建置語意。
開始前,先確認工作實際使用的開發者目錄,避免互動式工作階段與自動化工作選用了不同版本的 Xcode:
xcode-select -p
xcodebuild -version
xcrun --find ibtool
如果同一台機器保留多個 Xcode,請勿在工作中反覆修改全域選擇。可針對單次工作明確設定 DEVELOPER_DIR,並讓預檢與後續建置使用相同的值。
批次編譯 Storyboard 與 XIB
以下指令碼會遞迴尋找工程中的介面檔案,並略過相依套件、建置產物及快取目錄。它會根據相對路徑產生雜湊,因此即使不同模組都含有 Main.storyboard,輸出也不會互相衝突。
#!/bin/bash
set -euo pipefail
export LANG=C
export LC_ALL=C
ROOT="${1:-$PWD}"
TMP="$(mktemp -d "${TMPDIR:-/tmp}/ibtool-check.XXXXXX")"
trap 'rm -rf "$TMP"' EXIT
status=0
while IFS= read -r -d '' file; do
rel="${file#"$ROOT"/}"
key="$(printf '%s' "$rel" | shasum -a 256 | cut -c1-12)"
log="$TMP/$key.log"
case "$file" in
*.storyboard) output="$TMP/$key.storyboardc" ;;
*.xib) output="$TMP/$key.nib" ;;
*) continue ;;
esac
printf 'Checking %s
' "$rel"
if ! xcrun ibtool \
--errors \
--warnings \
--notices \
--compile "$output" \
"$file" >"$log" 2>&1; then
cat "$log"
status=1
fi
done < <(
find "$ROOT" \
\( -path '*/Pods/*' \
-o -path '*/Carthage/*' \
-o -path '*/.build/*' \
-o -path '*/DerivedData/*' \) -prune \
-o \( -name '*.storyboard' -o -name '*.xib' \) \
-type f -print0
)
exit "$status"
-print0 與 read -d '' 能正確處理含有空格的路徑。暫存目錄會透過 trap 清除,不會將編譯產物留在工作區。只有對應檔案發生錯誤時才會輸出失敗記錄,避免大量成功訊息淹沒 CI 記錄。
不要直接編譯到原始碼目錄
.storyboardc 實際上是目錄,.nib 也可能包含多個編譯結果。如果將輸出寫回原始碼附近,後續的 Git 狀態、資源掃描及封裝步驟都可能受到干擾。所有預檢產物都應寫入該工作專用的暫存目錄,並在結束時清除。
同時檢查在地化資源
採用 Base Localization 的專案通常會保留一份基礎 Storyboard,再為各語言維護 .strings。即使介面檔案能順利編譯,某個 strings 檔案仍可能因引號、逸出字元或衝突標記而損毀。可先使用 plutil 檢查格式:
find "$PWD" -type f \
\( -name '*.strings' -o -name '*.stringsdict' \) \
-not -path '*/Pods/*' \
-print0 |
while IFS= read -r -d '' file; do
plutil -lint "$file"
done
需要檢查基礎介面的可翻譯鍵值時,可產生一份暫時清單:
xcrun ibtool \
--generate-strings-file /tmp/Main.generated.strings \
App/Base.lproj/Main.storyboard
plutil -lint /tmp/Main.generated.strings
不要用產生的檔案直接覆寫人工維護的翻譯。產生順序與註解可能隨 Xcode 版本變化;更穩妥的做法是解析鍵值集合,確認新增鍵值是否已納入翻譯流程,以及已淘汰的鍵值是否仍被保留。
將診斷結果轉為可維護的門禁
預檢失敗時應立即停止目前階段,但警告則需要分級處理。版面配置相容性提示、缺少輔助使用標籤,以及舊屬性警告都值得記錄;然而,不同 Xcode 版本新增的診斷,不應在未經評估的情況下突然封鎖所有分支。
| 現象 | 常見原因 | 處理方式 |
|---|---|---|
| XML 解析失敗 | 合併衝突標記或檔案遭截斷 | 回到造成衝突的提交進行修復,避免手動刪除未知節點 |
| 輸出遭到覆寫 | 多個模組含有同名介面檔案 | 使用相對路徑雜湊產生輸出名稱 |
| 在地化檔案無法解析 | 引號、分號或逸出字元錯誤 | 對所有 strings 與 stringsdict 執行 plutil -lint |
| 本機通過、CI 失敗 | Xcode 或 DEVELOPER_DIR 不一致 |
在記錄中保留版本資訊,並統一工作環境 |
| 警告數量持續波動 | 工具版本變更或掃描範圍不穩定 | 固定排除目錄,並對新增警告進行差異檢查 |
小型變更可以只檢查目前分支中有異動的介面檔案;主分支或排程工作則應掃描整個儲存庫。增量檢查著重回饋速度,全量檢查則用於找出重新命名、刪除及跨模組調整造成的遺漏,兩者不應互相取代。
以完整建置完成最終驗收
ibtool 通過後,至少應執行一次不依賴簽署的模擬器建置,以確認介面資源能與程式碼、目標成員資格及其他資源共同運作:
: "${WORKSPACE:?Set WORKSPACE to the workspace path}"
: "${SCHEME:?Set SCHEME to the shared scheme}"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO \
build
最終門禁應確認預檢與完整建置使用相同的 Xcode、指令碼排除目錄與工程相依套件目錄一致、暫存產物不會進入儲存庫,並且失敗記錄保留原始相對路徑。如此一來,介面資源問題會先在數秒內完成的檢查中暴露,真正需要工程上下文的問題再進入完整建置,問題排查的邊界也會更加清楚。
常見問題
ibtool 預檢可以取代完整的 Xcode 建置嗎?
不可以。ibtool 適合檢查介面檔案本身,程式型別、建置設定、資源連結與最終套件仍應交由 xcodebuild 驗證。
為什麼輸出路徑不能只使用來源檔名?
不同模組可能同時存在 Main.storyboard 或 View.xib。只使用 basename 會互相覆寫,應保留相對路徑或依路徑產生雜湊。
所有 ibtool 警告都應該讓 CI 失敗嗎?
建議先建立經過檢視的既有警告基準,再阻擋新增警告。Xcode 更新可能改變診斷內容,直接阻擋全部警告容易產生雜訊。
為下一次建置選擇專屬 Apple Silicon 實體節點
按日、週、月或季租用 ArmVMS M4 與 ArmVMS M4 Pro。建立訂單前,可逐項確認設定、節點與附加項目。