多人同时修改 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"
-print0 与 read -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 适合快速发现界面文件自身的编译错误,但 Swift 或 Objective-C 类型、构建设置、资源目录和最终链接问题仍需由 xcodebuild 验证。
为什么脚本不能只按文件名生成输出目录?
不同模块可能存在同名的 Main.storyboard 或 View.xib。只使用 basename 会造成输出覆盖,应根据相对路径生成哈希或保留目录层级。
是否应该把所有 ibtool 警告都设为失败?
先记录并建立稳定基线,再只阻断新增警告更可靠。不同 Xcode 版本可能调整诊断文本,直接阻断全部历史警告容易制造无效失败。
为下一次构建选择独享 Apple Silicon 物理节点
按天、周、月或季租用 ArmVMS M4 与 ArmVMS M4 Pro。配置、节点和附加项目在创建订单前逐项确认。