Optimizing Asset Catalog Compilation in CocoaPods for Faster iOS Builds
November 01, 2024
Efficient asset management is crucial for maintaining optimal build times in iOS development. Asset catalogs (.xcassets
) are central to this process, but default behaviors in tools like CocoaPods can lead to unnecessary recompilation, slowing down your development cycle.
In this post, I'll delve into how asset catalogs are handled during the build process, the inefficiencies introduced by CocoaPods' default resource packaging logic, and how to optimize this process to achieve significantly faster incremental builds.
Understanding Asset Catalogs and CocoaPods
What Are Asset Catalogs?
Asset catalogs (.xcassets
) are collections of assets such as images, icons, and colors used in an iOS application. During the build process, these catalogs are compiled into a binary-optimized format using a tool called actool
. This compilation ensures that assets are efficiently packaged for the app.
actool
The Role of actool
is a command-line utility provided by Xcode that compiles asset catalogs into a format that the app can use at runtime. It also handles the extraction of the app's main icon during the build phase.
Asset Catalog Compilation
If an application has multiple asset catalogs, actool
compiles them together into a single binary asset catalog embedded in the application. This process is crucial for the app to access its assets efficiently.
How CocoaPods Handles Resources
CocoaPods is a dependency management system for Swift and Objective-C projects. It simplifies integrating third-party libraries into your app. When dealing with resources, CocoaPods must manage packages (pods) that provide assets, sometimes compiling them into bundles or including them directly in the main application's asset catalog.
The Problem: Inefficient Asset Catalog Compilation
Default Resource Packaging Logic
CocoaPods generates a resource copy script named Pods-<Target_Name>-resources.sh
for each target. This script is called on every build and contains logic to package resources from dependencies. However, this logic is minimal and introduces inefficiencies, particularly with asset catalogs.
Recompilation on Every Build
By default, CocoaPods lacks logic to prevent recompiling the asset catalog when the original resources haven't changed. Consequently, the asset catalog is rebuilt on every build, including incremental ones where no assets have been modified. This unnecessary recompilation increases build times and reduces productivity.
Redundancy in Resource Packaging
The default packaging logic has double redundancy:
- Always Recompiling the Binary Asset Catalog: Regardless of changes, the asset catalog is compiled every time.
- Packaging Resources Twice: Resources are included both in the bundles for local modules and again in the main module.
Naming Collisions
Asset catalogs share a global naming domain. If assets in different catalogs have the same name, the final binary asset catalog can only include one of them, leading to undefined behavior regarding which asset is used.
A Practical Example
Consider the following folder structure:
root_folder
|-- ExternalModules
| |-- Module1
| | |-- Assets.xcassets
| |-- Module2
| | |-- Assets.xcassets
| |-- Module3
| | |-- Assets.xcassets
|-- Sources
| |-- resources
| | |-- Assets.xcassets
Initial Resource Collection
Initially, resources are collected via the install_resource
bash function in the resource copy script. Notice how the contents of the .xcassets
catalogs are listed as individual files:
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Assets.xcassets/info-icon-blue.imageset/Contents.json"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Assets.xcassets/info-icon.imageset/Contents.json"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Assets.xcassets/shopping-power-icon1.imageset/Contents.json"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Assets.xcassets/shopping-power-icon2.imageset/Contents.json"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Assets.xcassets/shopping-power-icon3.imageset/Contents.json"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Localizable/ar.lproj/Localizable.strings"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Localizable/en.lproj/Localizable.strings"
install_resource "${PODS_ROOT}/../ExternalModules/SpendingLimit/Sources/SpendingLimit/Resources/Assets.xcassets"
The call to install_resource
with an .xcassets
argument stores the asset in the XCASSET_FILES
array.
CocoaPods Resource Copy Script
At the end of the script, CocoaPods scans for all .xcassets
files—even those not included in the packages:
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ]
then
# Find all other xcassets (this unfortunately includes those of path pods and other targets).
OTHER_XCASSETS=$(find -L "$PWD" -iname "*.xcassets" -type d)
while read line; do
if [[ $line != "${PODS_ROOT}*" ]]; then
XCASSET_FILES+=("$line")
fi
done <<<"$OTHER_XCASSETS"
if [ -z ${ASSETCATALOG_COMPILER_APPICON_NAME+x} ]; then
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool \
--output-format human-readable-text --notices --warnings \
--platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" \
${TARGET_DEVICE_ARGS} --compress-pngs \
--compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
else
printf "%s\0" "${XCASSET_FILES[@]}" | xargs -0 xcrun actool \
--output-format human-readable-text --notices --warnings \
--platform "${PLATFORM_NAME}" --minimum-deployment-target "${!DEPLOYMENT_TARGET_SETTING_NAME}" \
${TARGET_DEVICE_ARGS} --compress-pngs \
--compile "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" \
--app-icon "${ASSETCATALOG_COMPILER_APPICON_NAME}" \
--output-partial-info-plist "${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist"
fi
fi
In this script:
- CocoaPods tries to predict which
.xcassets
belong to the current project by scanning all folders. - There's no logic to prevent rebuilding if assets haven't changed.
- The script doesn't discern between assets belonging to different targets, leading to redundancy and potential naming collisions.
The Solution: Optimizing Asset Catalog Compilation
To address these inefficiencies, we can modify the resource copy script to prevent unnecessary recompilation and reduce redundancy.
Overview of Optimizations
- Check if Asset Catalogs Have Changed: Use an external script to determine if recompilation is necessary.
- Ensure App Icon Extraction: Even if assets haven't changed, the app icon must still be exported.
- Ignore Assets from External Modules: Exclude
.xcassets
fromExternalModules
to avoid redundancy. - Modify Search Paths: Search for
.xcassets
only in the mainSources
folder.
Step 1: Checking for Asset Changes
I introduce an external script Scripts/check_xcassets_changed.sh
that checks whether the asset catalogs have changed since the last build. This script uses mtree
, a macOS tool that creates a checksum of a directory.
Scripts/check_xcassets_changed.sh
Script
any_mtree_created=false
# Locate all xcassets files
for fn in `find -L "$PWD" -path "$PWD/ExternalModules/*.xcassets" -o -path "$PWD/Sources/*.xcassets" -type d`; do
echo "Checking $fn"
sha=$(echo -n "$fn" | shasum -a 256 | head -c 40)
filename=$(basename -- "$fn")
mtreefile="${BUILT_PRODUCTS_DIR}/.mtree.$sha.$filename"
# Check if checksum file exists
if [ ! -f "$mtreefile" ]; then
echo "Creating snapshot for $fn..."
# Create checksum file
mtree -c -K sha256digest -p "$fn" > "$mtreefile"
any_mtree_created=true
else
echo "Comparing $fn..."
# Compare existing tree against the checksum file
if ! mtree -f "$mtreefile" -K sha256digest -p "$fn"; then
echo "Changes detected in $fn"
# Recreate the checksum
mtree -c -K sha256digest -p "$fn" > "$mtreefile"
exit 1
fi
fi
done
if [[ $any_mtree_created == true ]]; then
echo "New mtree files created, indicating change"
exit 1
fi
This script:
- Finds all
.xcassets
files belonging to the main target. - Generates or updates checksum files for each asset catalog.
- Determines whether recompilation is necessary based on checksum comparisons.
Step 2: Ensuring App Icon Extraction
Even if the asset catalogs haven't changed, we need to ensure that the app icon is correctly exported. We modify the script to handle this separately.
Step 3: Ignoring External Module Assets
We adjust the script to ignore .xcassets
from the ExternalModules
folder, as they are included in the bundles for local dependencies, avoiding redundancy and naming collisions.
Step 4: Modifying Search Paths
We modify the search paths in the script to look for .xcassets
only in the main Sources
folder, excluding unnecessary directories.
Patching the Resource Copy Script
We apply the optimizations directly to the Pods-<Target_Name>-resources.sh
script during the execution of the pod install
command. The code to patch the script is as follows:
# Patch "*-resources.sh" scripts to include optimized xcassets caching logic
resources_sh_files = [installer_representation.sandbox.root.join("Target Support Files/Pods-<TARGET NAME>/Pods-<TARGET NAME>-resources.sh")]
puts "Patching *-resources.sh scripts"
resources_sh_files.each do |resources_sh_file|
script = File.read(resources_sh_file)
File.open(resources_sh_file, 'w') do |f|
# Step 1: Insert the script to check if asset catalog recompilation is necessary
script = script.gsub(
'if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ]',
'if ! sh "${PODS_ROOT}/../Scripts/check_xcassets_changed.sh"; then
echo "XCAssets changed, processing..."
if [[ -n "${WRAPPER_EXTENSION}" ]] && [ "`xcrun --find actool`" ] && [ -n "${XCASSET_FILES:-}" ]'
)
# Step 2: Insert the logic to update app icon
script = script.gsub(
'assetcatalog_generated_info_cocoapods.plist"
fi
fi',
'assetcatalog_generated_info_cocoapods.plist"
fi
fi
else
echo "XCAssets not changed, skipping..."
fi
/usr/libexec/PlistBuddy -x -c "Merge \'${TARGET_TEMP_DIR}/assetcatalog_generated_info_cocoapods.plist\'" "${BUILT_PRODUCTS_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Info.plist"'
)
# Step 3: Skip xcassets from modules
script = script.gsub(
'install_resource()
{',
'install_resource()
{
[[ "$1" == *"ExternalModules"* && "$1" == *.xcassets ]] && { echo "Skipping $1"; return; }'
)
# Step 4: Search for assets in Sources
script = script.gsub(
'OTHER_XCASSETS=$(find -L "$PWD" -iname "*.xcassets" -type d)',
'OTHER_XCASSETS=$(find -L "${PODS_ROOT}/../Sources/" -iname "*.xcassets" -type d)'
)
f.puts(script)
puts "Patched #{resources_sh_file}"
end
end
This code:
- Step 1: Inserts the check for asset changes using the external script.
- Step 2: Ensures the app icon is updated even if assets haven't changed.
- Step 3: Skips
.xcassets
fromExternalModules
during resource installation. - Step 4: Adjusts the search path to include only the main
Sources
folder.
Results and Benefits
Implementing these optimizations led to significant improvements:
- Reduced Rebuild Time: Build times decreased from 20 seconds to 8 seconds for incremental builds.
- No Impact on CI Builds: Continuous Integration (CI) builds remain unaffected since they build the app from scratch each time.
- Avoided Naming Collisions: By excluding assets from
ExternalModules
, we reduced the risk of naming collisions in the final binary asset catalog. - Streamlined Asset Compilation: Only necessary assets are compiled, enhancing efficiency.
Conclusion
Optimizing asset catalog compilation in your iOS projects can lead to substantial improvements in build times and developer productivity. By customizing the resource copy script generated by CocoaPods and implementing a mechanism to detect changes in assets, we can prevent unnecessary recompilation and eliminate redundancy.
If you're experiencing long build times due to asset catalog compilation, consider implementing these optimizations. Not only will you save time, but you'll also create a more efficient and maintainable build process.
References: