When multiple developers modify Storyboards or XIBs at the same time, an XML merge may appear successful even though the real problems do not surface until a full build: a connected object has been deleted, an interface file no longer compiles, a localized strings file is malformed, or identically named outputs from two modules overwrite each other. Instead of waiting for a complete build on an ArmVMS cloud Mac to expose the failure, run an independent preflight check with Xcode’s built-in ibtool.
What ibtool can check
ibtool is the command-line compiler for Interface Builder resources. It can process .storyboard and .xib files independently, reporting problems with XML structure, object connections, selected property settings, and warnings raised during compilation. This check usually does not require the application code to be compiled first, making it well suited to pre-commit checks or an early CI stage.
It does not replace a complete build. Whether custom classes actually exist, Swift types match, resources belong to the correct target, and final linking succeeds must still be verified with xcodebuild. The appropriate sequence is to run ibtool first, then build and test the target project after the preflight passes.
Treat ibtool as a syntax and structure checker for interface resources, not as the final acceptance test. Its value lies in failing fast, not in covering every aspect of build semantics.
Before starting, confirm which developer directory the job is actually using. This prevents interactive sessions and automated jobs from selecting different Xcode installations:
xcode-select -p
xcodebuild -version
xcrun --find ibtool
If a machine has multiple Xcode installations, avoid repeatedly changing the global selection inside jobs. Set DEVELOPER_DIR explicitly for each job instead, and use the same value for both the preflight and the subsequent build.
Batch-compiling Storyboards and XIBs
The following script recursively finds interface files in the project while skipping dependency, build output, and cache directories. It derives each output name from a hash of the relative path, so outputs will not collide even when multiple modules contain a 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"
The combination of -print0 and read -d '' correctly handles paths containing spaces. The temporary directory is removed through trap, so compiled artifacts are not left in the working tree. A log is printed only when its corresponding file fails, keeping CI output from being overwhelmed by successful checks.
Do not compile directly into the source directory
A .storyboardc is actually a directory, and a .nib may also contain multiple compiled outputs. Writing these results alongside the source files can interfere with Git status, resource scanning, and later packaging steps. All preflight artifacts should go into a job-specific temporary directory and be removed when the job exits.
Checking localization resources at the same time
Projects that use Base Localization typically keep one base Storyboard and maintain separate .strings files for each language. The interface file may compile successfully even when one strings file is corrupted by malformed quotes, escape sequences, or merge conflict markers. Start by validating the format with 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
To review the translatable keys in the base interface, generate a temporary inventory:
xcrun ibtool \
--generate-strings-file /tmp/Main.generated.strings \
App/Base.lproj/Main.storyboard
plutil -lint /tmp/Main.generated.strings
Do not overwrite manually maintained translations with the generated file. Entry order and comments may change between Xcode versions. A safer approach is to parse the key set and verify that new keys have entered the translation workflow and obsolete keys have not been retained.
Turning diagnostics into a maintainable CI gate
A failed preflight should stop the current stage immediately, but warnings need to be classified. Layout compatibility notices, missing accessibility labels, and deprecated property warnings are worth recording. Diagnostics newly introduced by a different Xcode version, however, should not suddenly block every branch before they have been evaluated.
| Symptom | Common cause | Resolution |
|---|---|---|
| XML parsing fails | Merge conflict markers or a truncated file | Return to the conflicting commit and fix it without manually deleting unknown nodes |
| Output is overwritten | Multiple modules contain interface files with the same name | Generate output names from hashes of relative paths |
| Localization file cannot be parsed | Invalid quotes, semicolons, or escape sequences | Run plutil -lint on every strings and stringsdict file |
| Passes locally but fails in CI | Xcode or DEVELOPER_DIR differs |
Record the version in the logs and standardize the job environment |
| Warning counts keep changing | The tool version changed or the scan scope is unstable | Fix the excluded directories and review newly introduced warnings as a diff |
For small changes, you can check only the interface files modified on the current branch. The main branch and scheduled jobs should scan the entire repository. Incremental checks provide fast feedback, while full scans catch omissions caused by renames, deletions, and cross-module changes. Neither should replace the other.
Completing final validation with a full build
After ibtool passes, run at least one simulator build that does not require code signing. This verifies that the interface resources work together with the code, target membership, and other resources:
: "${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
The final gate should confirm that the preflight and complete build use the same Xcode installation, the script’s excluded directories match the project’s dependency directories, temporary artifacts cannot enter the repository, and failure logs preserve the original relative paths. Interface resource problems will then surface in a check that takes only seconds, while issues requiring full project context proceed to the complete build, making the troubleshooting boundary much clearer.
Frequently asked questions
Can an ibtool preflight replace a complete Xcode build?
No. It catches problems inside interface files, while source types, build settings, linked resources, and the final bundle still require validation with xcodebuild.
Why should outputs not be named with the source basename alone?
Separate modules can contain identically named Main.storyboard or View.xib files. Hashing the relative path or preserving its directory structure prevents output collisions.
Should every ibtool warning fail the CI job?
Start with a reviewed warning baseline and block only new warnings. Diagnostic wording can change between Xcode releases, so failing all existing warnings often creates noise.
Choose a dedicated Apple Silicon physical node for your next build
Rent ArmVMS M4 and ArmVMS M4 Pro by the day, week, month, or quarter. Review the configuration, node, and add-ons before creating your order.