工程實務

在雲端 Mac 用 ibtool 預檢 Storyboard 與 XIB

在雲端 Mac 用 ibtool 預檢 Storyboard 與 XIB

多人同時修改 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"

-print0read -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 更新可能改變診斷內容,直接阻擋全部警告容易產生雜訊。

ArmVMS 雲端 Mac

為下一次建置選擇專屬 Apple Silicon 實體節點

按日、週、月或季租用 ArmVMS M4 與 ArmVMS M4 Pro。建立訂單前,可逐項確認設定、節點與附加項目。

選擇設定並建立訂單