Moving levels, high-resolution textures, or offline models in an iOS project to On-Demand Resources usually makes the installation package significantly smaller. A successful build, however, does not guarantee that the resources are available. The most common failures are not compiler errors but missing tag assignments, test builds that accidentally embed every resource, or resource packs in the release artifact that do not match the requests made by the code. A cloud Mac is well suited to running these checks as a dedicated job: archive from a clean workspace every time, retain manifests and checksums, and then perform a real resource request.
Define testable acceptance criteria first
ODR acceptance testing should cover at least three layers: project configuration, exported artifacts, and runtime requests. Checking only one layer leaves blind spots.
At the project level, confirm that On-Demand Resources are enabled for the Target and that every tag in the resource catalog has a clearly defined purpose. At the artifact level, inspect .assetpack items, manifest files, and file hashes. At runtime, verify the initial request, resource release, and a subsequent request.
An application that launches successfully has not necessarily passed ODR acceptance testing. Even if an unvisited level is missing all of its resources, the app can still open its initial screen.
For each tag, record four details: the owner, the screen that triggers it, the expected resource type, and whether it is required for the initial installation. Store this information in a plain-text manifest in the repository rather than leaving it only in the Xcode interface.
Use stable tags to control package granularity
Do not map tags directly to individual folders. Splitting resources too finely produces many small packs and frequent requests, while grouping them too broadly forces users to download an entire set of unrelated resources for a single screen. A more reliable approach is to organize tags around user flows, such as level-01, tutorial-audio, and model-basic, and ensure that request names in the code exactly match the tags in the resource catalog.
Enforce a tag manifest gate
The repository can contain an allowlist of tags:
level-01
level-02
tutorial-audio
model-basic
The build script should extract tags from the project file or generated resource metadata, sort them, and compare them with this manifest. Adding a tag must include a corresponding manifest update. When removing a tag, also check the NSBundleResourceRequest calls in the code. This moves spelling-error detection from runtime to the merge stage.
Tag names should use only stable ASCII characters. Avoid spaces, inconsistent capitalization, and temporary version numbers. Represent resource updates through content versions or checksums instead of continually creating unmaintainable names such as level-final-v2-new.
Standardize archive and export conditions
The same commit must use a fixed workspace, scheme, Release configuration, and set of export options. A job on a VMArm node can clear its output directory before starting, but it should not indiscriminately delete global caches. Otherwise, it becomes difficult to determine whether a change came from the resources or the environment.
set -euo pipefail
: "${WORKSPACE:?WORKSPACE is required}"
: "${SCHEME:?SCHEME is required}"
ROOT="$PWD"
ARCHIVE_PATH="$ROOT/out/Game.xcarchive"
EXPORT_DIR="$ROOT/out/export"
rm -rf "$ARCHIVE_PATH" "$EXPORT_DIR"
mkdir -p "$EXPORT_DIR"
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE_PATH" \
clean archive
xcodebuild \
-exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "$ROOT/ci/ExportOptions.plist"
For local acceptance testing, set embedOnDemandResourcesAssetPacksInBundle to true to first confirm that the split-pack content and request logic are correct. The production path must use the export configuration for the actual distribution method rather than reusing the local embedding settings. Save artifacts from the two paths in separate directories so that a test build cannot overwrite the package intended for release.
Inspect artifacts at three levels
The first level checks that files exist. The second verifies that plist files can be parsed. The third generates stable checksums for all resource files so that two builds of the same commit can be compared.
set -euo pipefail
ARCHIVE_PATH="$PWD/out/Game.xcarchive"
EXPORT_DIR="$PWD/out/export"
REPORT_DIR="$PWD/out/report"
mkdir -p "$REPORT_DIR"
find "$ARCHIVE_PATH" "$EXPORT_DIR" \
-name '*.assetpack' \
-print | LC_ALL=C sort > "$REPORT_DIR/assetpacks.txt"
find "$ARCHIVE_PATH" "$EXPORT_DIR" \
-name 'AssetPackManifest.plist' \
-exec plutil -lint {} \; > "$REPORT_DIR/plist-lint.txt"
find "$ARCHIVE_PATH" "$EXPORT_DIR" \
-type f \
\( -name '*.car' -o -name '*.plist' -o -name '*.assetpack' \) \
-exec shasum -a 256 {} \; |
LC_ALL=C sort > "$REPORT_DIR/checksums.txt"
test -s "$REPORT_DIR/assetpacks.txt"
If an .assetpack is actually a directory, the final hashing command will not calculate hashes for its contents directly. Regular files under that directory must therefore be included in the check as well. The report should record the commit, Xcode version, scheme, and hash of the export configuration file, but it must not include signing private keys, tokens, or full credential paths.
When comparing two reports, first distinguish changes in file order from changes in content. Consistent sorting eliminates the former. If hashes for the same commit still change repeatedly, check whether a generation script is writing timestamps, absolute workspace paths, or random identifiers.
Validate runtime behavior and diagnose failures
Runtime testing must start from a clean state. After installing the acceptance build, request a tag that is not required for the initial installation and record three events: the start of the request, access becoming available, and release of the resources. Then terminate the app, reset the test environment, and repeat the process to confirm that the successful request did not come from an old cache.
Troubleshooting order for common symptoms
If a request fails immediately, first compare the tag in the code with the tag in the resource catalog. If a request remains pending for a long time, check whether the resource pack was exported correctly and verify the test network. If local embedding succeeds but the production path fails, focus on differences between the two ExportOptions files and the resource-hosting configuration. If only some resources are missing, check whether files with the same name were assigned to multiple tags or whether the resources still belong to the main bundle.
Do not hide errors behind unlimited retries. Set an explicit timeout for each request. Failure logs should record only the tag, error domain, error code, and stage; they must not expose access tokens or sensitive paths. After acceptance testing, retain the report, export configuration hash, and failure logs, then remove temporary packages and test caches. When a later ODR change causes a discrepancy, you can compare the artifacts directly instead of guessing again what Xcode did.
Frequently asked questions
Should ODR asset packs be embedded in a development build?
Yes, for an initial local validation lane. Set embedOnDemandResourcesAssetPacksInBundle to true to isolate packaging errors from network delivery, then run a separate production-style export with the real delivery settings.
Is checking the IPA size enough to validate ODR?
No. Validate asset-pack counts, tag mappings, AssetPackManifest.plist files, checksums, and resource requests from a clean state. Repeat the request after cache removal to verify that success was not caused by retained data.
Choose an always-on cloud Mac for your next Xcode build
Check the chip, memory, storage, billing cycle, and node region before starting configuration. Availability is based on the live status returned by the console.