エンジニアリング実践

クラウドMacでStoryboardとXIBをibtool事前検査する

クラウドMacでStoryboardとXIBをibtool事前検査する

複数人がStoryboardやXIBを同時に変更すると、XMLのマージ自体は成功しているように見えても、実際の問題は完全なアーカイブを作成する段階で初めて表面化することがあります。接続先のオブジェクトが削除されている、インターフェースファイルをコンパイルできない、ローカライズ用stringsの形式が壊れている、あるいは別々のモジュールにある同名の出力が上書きし合う、といった問題です。ArmVMS上のクラウドMacでフルビルドを実行してからエラーに気付くよりも、まずXcode付属のibtoolで独立した事前検査を行う方が効率的です。

ibtoolで検査できる範囲を確認する

ibtoolは、Interface Builderリソース用のコマンドラインコンパイラです。.storyboard.xibを個別に読み込み、XML構造、オブジェクト接続、一部の属性設定、およびコンパイル時の警告を報告できます。通常はアプリケーションコードを先にコンパイルする必要がないため、コミット時のチェックやCIの初期段階に組み込むのに適しています。

ただし、フルビルドの代わりにはなりません。カスタムクラスが実際に存在するか、Swiftの型が一致しているか、リソースが正しいターゲットに含まれているか、最終リンクが完了するかといった点は、引き続きxcodebuildで確認する必要があります。先にibtoolを実行し、通過後に対象プロジェクトのビルドとテストを行うのが適切な順序です。

ibtoolは最終成果物の受け入れ検査ツールではなく、インターフェースリソースの構文・構造チェッカーとして扱います。価値があるのは早期に失敗を検出できる点であり、ビルド全体の意味論を網羅することではありません。

開始前に、ジョブが実際に使用しているDeveloperディレクトリを確認します。これにより、対話セッションと自動化ジョブで異なるXcodeが選択される事態を防げます。

xcode-select -p
xcodebuild -version
xcrun --find ibtool

1台のマシンに複数の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を1つ保持し、言語ごとに.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を通過した後は、署名に依存しないシミュレータ向けビルドを少なくとも1回実行し、インターフェースリソース、コード、ターゲットメンバーシップ、その他のリソースが連携して動作することを確認します。

: "${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が存在すると、生成物が上書きされるためです。相対パスのハッシュか元の階層を使います。

ibtoolの警告はすべて失敗扱いにするべきですか?

最初に既存警告の基準を作り、新規警告だけを止める運用が現実的です。Xcode更新で診断文が変わる可能性も考慮します。

ArmVMS クラウドMac

次のビルドに専用のApple Silicon物理ノードを選択

ArmVMS M4とArmVMS M4 Proを、日単位、週単位、月単位、または四半期単位でレンタルできます。注文を作成する前に、構成、ノード、追加項目を一つずつ確認できます。

構成を選択して注文を作成