Engineering Article

Preflight Storyboards and XIB with ibtool on Cloud Mac

Preflight Storyboards and XIB with ibtool on Cloud Mac

A Storyboard may open without issue on a development machine, yet cause a remote archive job to fail halfway through after a merge. Redownloading dependencies, launching a simulator, or clearing DerivedData will not help if the actual cause is a broken outlet, an incorrect custom-class module, or an XIB property unsupported by the current deployment target. For continuous integration on a Cloud Mac, a more efficient approach is to compile interface resources separately with ibtool before deciding whether to proceed with the full build.

Move interface resource checks ahead of the build

ibtool is included with Xcode and should be invoked through xcrun rather than by hard-coding the tool path in a script. It can read Storyboards and XIB files, report errors, warnings, and notices, and compile source files into storyboardc or nib output. This stage does not require launching a simulator, making it suitable for execution after dependency resolution and before xcodebuild archive.

First, verify which developer directory the runner is actually using:

set -euo pipefail

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

Record this information in the job log. If the team keeps multiple Xcode versions on the same Cloud Mac, the pipeline must set DEVELOPER_DIR explicitly and unset that environment variable when the job finishes. Otherwise, subsequent jobs may inherit the wrong toolchain.

An interface file opening successfully in the graphical editor does not mean it will compile with the specified Xcode version, deployment target, and module context. The purpose of the CI check is to reproduce the release toolchain, not an individual developer’s desktop environment.

Scan and compile Storyboards and XIB files independently

The following script scans the repository for interface files and creates a separate output directory for each input. Excluding build and dependency directories is important because otherwise generated files or third-party resources may be checked more than once.

#!/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

The script uses a temporary directory and does not modify the repository. If the check fails, retain ibtool.log. If it succeeds, the compiled output can be deleted and only a log summary uploaded. Do not commit temporary output to version control, and do not let parallel jobs share a fixed directory.

Understand the boundary between isolated preflight checks and project builds

Running ibtool independently can detect malformed XML, some broken connections, incompatible properties, and compilation errors, but it does not know every build setting of the complete target. Questions such as which module contains a custom view controller, whether a resource is included in Copy Bundle Resources, and whether a conditionally compiled class exists must still be validated by xcodebuild.

Use two layers of checks:

Layer Input Primary findings Failure handling
Fast preflight Storyboard, XIB File corruption, incompatible properties, basic connection errors Stop immediately
Project build workspace, project, Scheme Module resolution, resource membership, linking and signing context Preserve the complete build log

If the isolated preflight passes but the project build fails, check Target Membership, the Module field, class-renaming history, and deployment-target inheritance first. Conversely, if the preflight has already failed, there is no reason to continue with the more time-consuming archive step.

Include localization files in the same quality gate

Storyboard localization usually takes one of two forms: separate resources maintained for each language, or Base Internationalization combined with .strings files. The former is prone to object ID drift, while the latter can retain keys for objects that have already been deleted. Start by using plutil to validate the basic format of .strings files:

find . -name "*.strings" -print0 |
while IFS= read -r -d '' file; do
  plutil -lint "$file"
done

For a Base Storyboard, generate the current set of keys and compare it with the localization files in the repository:

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"

The generated file is for comparison only and should not overwrite translated content directly. A safer workflow extracts and sorts the keys before calculating the differences: new keys are added to the translation queue, deleted keys produce cleanup notices, and text for existing keys remains under the localization workflow’s control. This detects omissions without accidentally replacing existing translations with Base text.

Manage warning baselines and common false positives

Treating all output as a failure may appear strict in the short term, but it eventually trains the team to ignore red builds. A more practical policy is to fail immediately on errors, compare warnings against a reviewed baseline stored in the repository and block only new ones, and preserve notices in the build artifacts for periodic cleanup.

Use the following troubleshooting order:

  1. Confirm that DEVELOPER_DIR matches the full build.
  2. Verify that IPHONEOS_DEPLOYMENT_TARGET comes from the project’s actual settings.
  3. Check path capitalization, especially for resources renamed only by changing letter case.
  4. Search for broken outlets, actions, and custom class names.
  5. Confirm that temporary directories are isolated per job so parallel compilations cannot overwrite one another’s output.
  6. Run another unsigned build with the same Scheme to validate the module context.

The warning baseline should store stable identifiers rather than temporary absolute paths. When upgrading Xcode, run the preflight check on a separate branch first, review the new diagnostics, and then update the baseline. Do not simply filter out all new warnings in the upgrade commit.

Establish a rollout sequence that can be reverted

For the initial integration, collect logs for one week without blocking merges. After identifying the sources of false positives, make deterministic errors fatal, then gradually enable gating for new warnings. Keep the script itself in the repository and use the same implementation on local and Cloud Macs rather than hiding a second version in the CI configuration.

The final checklist should confirm that the toolchain is pinned, scanned directories have explicit exclusions, every job uses an isolated temporary directory, the deployment target comes from project configuration, logs are retained as failure attachments, localization keys are compared but never overwritten, and the complete Scheme build still runs. With this setup, interface resource problems surface during a preflight stage that takes only tens of seconds instead of leaving a hard-to-reproduce error at the end of the archive process.

Frequently asked questions

Can an ibtool preflight replace a complete Xcode build?

No. It provides a fast structural and compilation check for interface resources, but the real project Scheme must still build and run its tests.

Why can a Storyboard open locally but fail in CI?

Typical causes include a different Xcode selection, a mismatched deployment target, path-case errors, or an unavailable module containing custom classes.

Should every ibtool notice fail the pipeline?

No. Fail on errors first, compare warnings with a reviewed baseline, and preserve notices as diagnostic artifacts before tightening the policy.

VMMini M4

Run Your Next Task on a Dedicated Physical Node

Choose a node and billing cycle, then start deploying with the fixed M4, 16GB RAM, and 256GB SSD configuration. Actual availability is based on the console's real-time status.

Rent a Cloud Mac Now