工程文章

雲端 Mac 用 ibtool 預檢 Storyboard 與 XIB

雲端 Mac 用 ibtool 預檢 Storyboard 與 XIB

Storyboard 在開發機上可以正常開啟,合併後卻可能讓遠端封存工作執行到一半才失敗。這時重新下載相依套件、啟動模擬器或清除 DerivedData 都無濟於事,因為真正的問題可能只是失效的 outlet、錯誤的自訂類別模組,或某個 XIB 使用了目前部署目標不支援的屬性。對雲端 Mac 上的持續整合而言,更有效率的做法是先以 ibtool 個別編譯介面資源,再決定是否進入完整建置。

將介面資源檢查提前到建置前

ibtool 隨 Xcode 提供,應透過 xcrun 呼叫,而不是在腳本中寫死工具路徑。它能讀取 Storyboard 與 XIB,輸出 errors、warnings 和 notices,並將來源檔案編譯成 storyboardcnib。這個階段不需要啟動模擬器,適合安排在解析相依套件之後、執行 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 則保留在建置產出中並定期清理。

常見的疑難排解順序如下:

  1. 確認 DEVELOPER_DIR 與完整建置一致。
  2. 核對 IPHONEOS_DEPLOYMENT_TARGET 是否來自專案的實際設定。
  3. 檢查檔案路徑的大小寫,尤其是重新命名後只變更字母大小寫的資源。
  4. 搜尋失效的 outlet、action 與自訂類別名稱。
  5. 確認暫存目錄依工作隔離,避免平行編譯相互覆寫輸出。
  6. 使用相同的 Scheme 再執行一次不簽署的建置,以驗證模組環境。

警告基準應保存穩定的識別資訊,避免記錄暫時性的絕對路徑。升級 Xcode 時,先在獨立分支執行預檢,審查新增的診斷訊息,再更新基準;不要在升級提交中直接篩除所有新警告。

建立可回復的導入順序

首次導入時,可以只收集一週的日誌,不阻擋合併。確認誤報來源後,再將具確定性的 errors 設為失敗,接著逐步啟用新增 warning 的門檻。腳本本身應固定存放於儲存庫中,供本機與雲端 Mac 共用,避免 CI 設定中藏有另一套實作。

最終檢查項目包括:工具鏈已固定;掃描目錄有明確的排除項目;每個工作使用獨立的暫存目錄;部署目標來自專案設定;日誌在失敗時作為附件保留;本地化鍵只比較而不覆寫;完整的 Scheme 建置仍會執行。如此一來,介面資源問題便能在數十秒的預檢階段顯現,而不是等到封存尾聲才留下難以重現的錯誤。

常見問題

ibtool 預檢可以取代完整的 Xcode 建置嗎?

不可以。它適合快速檢查介面資源結構與編譯相容性,最後仍須使用專案的實際 Scheme 完成建置與測試。

為什麼本機能開啟的 Storyboard 在 CI 仍會失敗?

常見原因是 CI 選到不同的 Xcode、部署目標不一致、路徑大小寫錯誤,或自訂類別所在模組未進入目前的建置環境。

是否應該把 ibtool 的 notices 全部視為失敗?

不建議。先以 errors 阻擋流程,讓 warnings 與既有基準比較,並將 notices 保存為診斷紀錄。

VMMini M4

在專屬實體節點上執行下一項任務

選擇節點與計費週期,使用固定的 M4、16GB RAM 和 256GB SSD 設定開始部署。實際可用性以控制台即時回傳為準。

立即租用雲端 Mac