Highlight
Distributing game and large app resources has long been a pain: bundling everything into the main app makes it too large (with a 4GB limit), while On-Demand Resources requires updating alongside the app. The Managed mode of Background Assets solves these problems.
Core Content
Building a game with tutorial levels forces developers into an awkward choice. Either bundle all assets into the main app, so users wait hundreds of MB or even GB before playing, with most platforms capping at 4GB; or use On-Demand Resources, but updating a single asset file means resubmitting the entire app. There’s a third path—write your own download logic with URLSession and handle retries, resumption, and version coordination yourself (01:54).
WWDC25’s solution is Managed Background Assets with Apple hosting. Assets are split into multiple asset packs, each with its own download policy: Essential downloads with app installation, Prefetch continues in the background after install, On-demand waits for code requests (04:00). The system provides a ready-made downloader extension—zero code to integrate. Apple Developer Program members get 200GB free hosting; asset packs and app builds upload separately in App Store Connect with separate review and version management, so updating assets no longer requires a new app release (03:36). Tutorial levels use Essential + firstInstallation so new users get them on install, while existing users don’t re-download on updates.
Detailed Content
First step is writing the manifest. xcrun ba-package template generates a JSON template. Fill in the asset pack ID, download policy, platforms, and file selectors (08:26).
{
"assetPackID": "Tutorial",
"downloadPolicy": {
"essential": {
"installationEventTypes": [
"firstInstallation",
"subsequentUpdate"
]
}
},
"fileSelectors": [
{ "file": "[Path to File]" },
{ "directory": "[Path to Directory]" }
],
"platforms": ["iOS", "macOS", "tvOS", "visionOS"]
}
Key points:
assetPackID: a custom string; runtime code uses it to reference this pack.downloadPolicyis one of three:essential,prefetch,onDemand. The first two require aninstallationEventTypesarray;onDemandtakes an empty object{}.installationEventTypesdetermines when to download:firstInstallationonly on first install,subsequentUpdatealso on upgrades.fileSelectorscan be added arbitrarily, by file or by directory; paths are relative to the source root.platformslists which platforms this pack supports; multiple platforms can use the same pack.
After writing the manifest, run ba-package again to bundle the content into an .aar archive (09:53).
Next, add a Background Download template extension target in Xcode. This year’s new system-level downloader extension requires almost no code (10:44).
import BackgroundAssets
import ExtensionFoundation
import StoreKit
@main
struct DownloaderExtension: StoreDownloaderExtension {
func shouldDownload(_ assetPack: AssetPack) -> Bool {
return true
}
}
Key points:
StoreDownloaderExtensionis the protocol for Apple-hosted scenarios; self-hosting usesBackgroundDownloaderExtension.shouldDownload(_:)is called once before scheduling each new pack; returningfalseskips the download, useful for compatibility filtering.- If customization isn’t needed, the entire
shouldDownload(_:)can be deleted—the system default implementation handles everything.
The main app code to read asset packs is also brief (11:39):
let assetPack = try await AssetPackManager.shared.assetPack(withID: "Tutorial")
// Await status updates for progress information
let statusUpdates = AssetPackManager.shared.statusUpdates(forAssetPackWithID: "Tutorial")
Task {
for await statusUpdate in statusUpdates {
// …
}
}
// Download the asset pack
try await AssetPackManager.shared.ensureLocalAvailability(of: assetPack)
Key points:
AssetPackManager.sharedis the entry singleton; all operations start from it.assetPack(withID:)gets a pack handle; most of the time the downloader extension has already downloaded it in the background.statusUpdates(forAssetPackWithID:)returns anAsyncSequenceto get states like.downloading(_, progress)for UI in a loop.ensureLocalAvailability(of:)is the core method: returns immediately if already downloaded, waits for download otherwise. Rarely, it can take a while, so subscribe to status for progress display.
After downloading, read files with contents(at:) (12:41):
// Read a file into memory
let videoData = try AssetPackManager.shared.contents(at: "Videos/Introduction.m4v")
// Open a file descriptor
let videoDescriptor = try AssetPackManager.shared.descriptor(for: "Videos/Introduction.m4v")
defer {
do {
try videoDescriptor.close()
} catch {
// …
}
}
Key points:
contents(at:)paths are relative to the source root, matching your working directory when runningba-package. The system merges all asset packs into one shared namespace—when reading files, you don’t care which pack contains them.- By default returns memory-mapped
Data, suitable for large files without loading into memory all at once. descriptor(for:)gets a file descriptor for low-level reading; you mustclose()it yourself—usedefer.- Both methods accept an optional
searchingInAssetPackWithID:parameter to limit search to a single pack.
When packs are no longer needed, call remove(assetPackWithID:) to free space; you can re-download later with ensureLocalAvailability(of:) (13:56). The system automatically keeps downloaded packs up to date but won’t actively delete them; users can see your app’s space usage in Settings.
Finally, configuration (14:53):
<key>BAAppGroupID</key>
<string>group.com.naturelab.thecoast</string>
<key>BAHasManagedAssetPacks</key>
<true/>
<key>BAUsesAppleHosting</key>
<true/>
Key points:
BAAppGroupID: the main app and downloader extension must join the same App Group; the system uses this to coordinate between them.BAHasManagedAssetPacks: declares use of Managed mode so the system takes over download and updates.BAUsesAppleHosting: set totruewhen using Apple hosting; self-hosting requires additional keys likeBAManifestURL.
For local testing, use ba-serve to start a mock server. Downloads use HTTPS, so first create a root CA, install it on test devices, sign an SSL certificate, then start the mock server. On the device, enter the mock server address in Developer Settings > Development Overrides (15:29).
The release workflow is also separate. Upload asset packs via Transporter app, or use the App Store Connect REST API in three steps: POST backgroundAssets to create a pack record, POST backgroundAssetVersions to create a version, then upload .aar via backgroundAssetUploadFiles (18:47). The same asset pack can have multiple live versions: one each for App Store, TestFlight external, and TestFlight internal. The server selects and delivers the appropriate version based on app source (05:34). This means after updating an asset pack, all users still running older app versions automatically switch to the new asset pack, so test backward compatibility before releasing a new pack.
Core Takeaways
-
Extract a first-install Essential pack: Bundle tutorial levels and first-screen assets into one pack. Choose
essentialdownload policy withfirstInstallationso existing users don’t re-download on upgrades. New users play immediately after install—retention metrics will look better.- Why it’s worth doing: Game app main bundles start at 1GB; users drop off waiting for App Store downloads, then more drop off waiting for in-game loading. Essential merges these two downloads into the App Store progress bar—users perceive only one wait.
- How to start: Run
xcrun ba-package templateto get a manifest template, fill first-screen asset directory paths intofileSelectors, keep onlyfirstInstallationininstallationEventTypes, and use Apple hosting directly without setting up a server.
-
Convert DLC to On-demand packs with IAP: For paid chapters, limited-time event maps, holiday skins, change download policy to
onDemandand callensureLocalAvailability(of:)after successful purchase to trigger download.- Why it’s worth doing: Undownloaded DLC doesn’t occupy user device space; completed levels can be cleared with
remove(assetPackWithID:). Event versions push via new packs without app review, speeding up holiday operations. - How to start: Package each DLC as a separate pack; after purchase confirmation, fetch resources by pack ID. Monitor
.downloadingstatus fromstatusUpdates(forAssetPackWithID:)for progress display.
- Why it’s worth doing: Undownloaded DLC doesn’t occupy user device space; completed levels can be cleared with
-
Move on-device ML models to Background Assets: For iterated binary resources like CoreML models, vector indexes, embedding tables, release a separate pack. Model upgrades go through the asset pack channel instead of the app channel.
- Why it’s worth doing: Releasing the entire app for each model change is heavy; App Store review is slow. Asset pack review is faster, and users get new models without manually upgrading the app.
- How to start: Put model files in a separate directory, write a
directoryselector to package the whole directory, then usedescriptor(for:)to get a file descriptor to feed into the ML framework. Test on older app versions before releasing new models—old apps also auto-switch to new resources.
-
Migrate from On-Demand Resources: ODR is deprecated. Don’t use it in new projects; schedule migration for existing ones.
- Why it’s worth doing: ODR resources are tied to app builds—updating one image requires re-review. Background Assets decouples them, changing the development rhythm entirely.
- How to start: Re-cut ODR tags into asset packs by business function. Map download policies to original prefetch/on-demand behavior. Migrate one feature module at a time, test with mock server first, then move to TestFlight.
-
Use Apple’s 200GB hosting: Developer Program membership includes 200GB free hosting; save on self-hosted CDN bills.
- Why it’s worth doing: Running your own CDN means handling signatures, rate limits, and abuse prevention. Apple hosting takes all of this and automatically matches versions by platform and build.
- How to start: Add
BAUsesAppleHosting = trueto Info.plist, choose Apple hosting in the downloader extension template, and drag.aarfiles via Transporter.
Related Sessions
- Dive deeper into Writing Tools — integrate Writing Tools in your app for proofreading, rewriting, and transforming text.
- Dive into App Store server APIs for In-App Purchase — latest updates to App Store Server API and notifications, for use with DLC-style on-demand asset packs.
- Enhance child safety with PermissionKit — new framework for enhancing child communication safety with PermissionKit.
- Explore enhancements to your spatial business app — API enhancements visionOS 26 brings to spatial business apps.
Comments
GitHub Issues · utterances